美文网首页
Java面向对象

Java面向对象

作者: kim_liu | 来源:发表于2018-09-01 00:04 被阅读36次

    一、类和对象

    对象:对象是用计算机语言对问题域中事物的描述,对象通过属性和方法来分别对应事物具有的静态属性和动态属性。
    类:类是用于描述同一类型的对象的一个抽象的概念,类中定义了这一类对象所因具有的静态和动态属性。
    类可以看成一类对象的模版,对象可以看成该类的一个具体实例。
    如学生是类,学生中的小明即是对象。小明有什么属性呢,如:姓名,年龄。小明有什么动态属性呢?如:显示姓名,显示年龄。

    类与类之间的关系:
    1、关联关系:一个类中的属性,是另一个类的对象。
    2、继承关系:XX是一种XX。
    3、聚合关系:聚集(队员和队长聚集成球队)和组合(几者密不可分,如头、手等组合成身体)
    4、实现关系:跟接口有关。如果父类中的某个方法,每个子类都有不同的实现方式,那么把这个方法写在某个接口中,让子类去实现这个接口,重写接口中的方法。

    Java类的定义:使用class定义类,并且定义成员变量与方法。
    成员变量:成员变量可以是Java中任何一种数据类型,在定义成员变量时,可以对其初始化,也可以不对其初始化,如果不初始化,那么Java会使用默认的值对其初始化。这一点不同于方法中定义的局部变量,局部变量必须遵循先定义,再初始化,再使用的原则,如果不初始化直接使用,编译会报错。成员变量的作用范围为整个类。

    Java中的默认初始化: Java中的默认初始化

    Java中的引用:Java中除了基础类型之外的变量类型都称为引用类型。Java中的对象是通过引用对其操作的。

    在内存中,系统为基础类型的数据在栈中分配了一块内存。如int i = 0;在内存中分配如下: 基本数据类型的内存分配 该块内存叫做i,值为0。

    那么引用类型的变量,在内存中是如何分配的呢?
    引用类型的变量,在内存中占两块内存。比如说下面这段代码

    //声明了一个String类型的引用变量,但并没有使它指向一个对象。
    //s是成员变量,成员变量位于栈内存
    String s;
    //使用new语句创建了一个String类型的对象并用s指向它,
    //以后可以通过s完成对其的炒作
    s = new String("Hello World");
    
    可以这样解释: image.png

    这就是Java创建对象时,系统是如何为其分配内存的。

    为什么对象放在堆内存中呢?这是因为堆内存是动态分配的,而对象也是在程序运行期间才会创建的。

    如何在内存中区分类和对象呢?
    类是静态的概念,位于代码区。而对象是new出来的,位于堆内存,类的每个成员变量在不同的对象中有不同的值(除了静态变量),而方法只有一份,执行的时候才占内存。关于这部分的内容,后面会做详细的解释。

    对象的创建和使用:
    1.必须使用new关键字创建对象。
    2.使用对象.成员变量来引用对象的成员变量。
    3.使用对象.方法(参数列表)来调用对象的方法。
    4.同一个类的不同对象有不同的成员变量存储空间
    5.同一个类的不同对象共享该类的方法。

    使用代码和图解解释类和对象在内存中是如何分配的:

    class C{
    
       int I ;
       int j ;
    
      public static void main(String[] args){
          C c1 = new C();
          C c2 = new C();
    }
    
    }
    
    类和对象在内存中的详细解释

    如上图所示:类是静态的代码,存在于代码区。main方法中的c1 c2是局部变量,位于栈内存中,当new出对象时,c1,c2中的值是指向堆内存中对应对象的地址。而i 和 j是成员变量,位于堆内存中分配给对象的那块内存中。

    构造方法 : 使用new + 构造方法创建一个新的对象。构造方法必须与类同名,并且没有返回值。是用来初始化对象的函数。对构造函数的具体解释如下:

    class Person{
       int id ;
       int age;
    public Person(int _id,int _age){
       this.id = _id;
       this.age = _age;
    }
    public static void main(String[] args){
         Person tom = new Person(1,25);
    }
    }
    
    构造方法创建对象时,内存变化

    任何一个局部变量,都被分配在栈内存中,方法一旦执行完,局部变量被释放。
    当没有为类编写构造函数时,编译器自动为其添加无参的构造函数。

    实例: 对一小段代码进行内存分析:

    class BirthData{
        private int day;
        private int month;
        private int year;
    
    
        public BirthData(int d,int m,int y){
            day = d;
            month = m;
            year = y;
        }
    
        public void setDay(int d){
            day = d;
        }
    
        public int getDay(){
            return day;
        }
    
        public void setMonth(int m){
            month = m;
        }
    
        public int getMonth(){
            return month;
        }
    
        public void setYear(int y){
            year = y;
        }
    
        public int getYear(){
            return year;
        }
    
        public void display(){
            System.out.println(day +" - " + month + " - "+ year);
        }
    }
    
    public class Test{
        public static void main(String[] args){
            Test t = new Test();
            int date = 9;
            BirthData b1 = new BirthData(7,7,1970);
            BirthData b2 = new BirthData(1,1,2000);
            t.change1(date);
            t.change2(d1);
            t.change3(d2);
            System.out.println("date = " + date);
            d1.display();
            d2.display();
        }
    
        public void change1(int i){
            i = 1234;
        }
    
        public void change2(BirthData b){
            b = new BirthData(22,2,2004);
        }
    
        public void change3(BirthData b){
            b.setDay(22);
        }
    }
    
    运行该程序,结果如下: 运行结果

    诶?怎么跟我们想象中的不一样呢?data的值为什么没变还是9呢?b1怎么还是指向之前的对象呢?别急,一步步分析,分析完一切都明了了。
    分析:1.前4句代码


    前四句代码分析
    当前四句代码执行完毕,栈内存中为构造方法创建的三个局部变量消失。
    内存中的情况变为这样,如下图所示。
    图片.png
    1. test.change1(data);这句代码非常有迷惑性,一开始我也认为运行完这句代码,data的值为变为1234,其实不是,分析完就懂了,为什么经过这句代码,data的值并没有改变。


      图片.png

      当这句代码执行完成之后,内存中是这样的,如下图所示


      图片.png
      3.test.change2(b1); 调用change2,将b1传递给b,因此b一开始指向的是b1指向的对象,当走到b=new BirthData(22,2,2004);这句代码时,b指向了内存中的22,2,2004对象。
      图片.png
    当change2调用完毕,为其分配的局部变量b消失,在堆内存中留下一个没有引用的对象,等待垃圾回收器的回收。change2调用完之后,内存中的情况是这样。 图片.png

    4.test.change3(b2);


    图片.png
    这句代码执行完成内存中的情况如下:
    图片.png
    现在很清楚,打印结果应该是: 图片.png 没毛病~
    方法重载(OverLoad)

    一个类中可以定义有相同的名字,但参数不同的多个方法,调用时,会根据不同的参数列表选择不同的方法。
    返回值不同但参数相同,这种不叫做方法重载,这种叫做重名,编译是无法通过的,其实只要记住一句话就不会出错:只要程序在编译时能分清调用的是哪个方法,这几个方法就构成重载。

    this关键字

    this是一个引用,指向自身对象的引用。
    1.在类的方法定义中使用的this关键字代表使用该方法的对象的引用。
    2.当必须指出当前使用方法的对象是谁时要使用this。
    3.有时使用this可以处理方法中成员变量和参数重名的情况。
    4.this可以看作是一个变量,它的值是当前对象的引用。

    this关键字
    static关键字

    1.在类中,用static声明的成员变量为静态成员变量,它是该类的公用变量,在第一次使用时被初始化,对于该类的所有对象来说,static成员变量只有一份。(对比:非静态的成员变量,每new出一个就有一份。)

    1. 用static声明的方法为静态方法,在调用该方法时,不会将对象的引用传递给它,所以在static方法中不可访问非static的成员。(静态方法不再是针对于某个对象调用,所以不能访问非静态成员)(对比:非静态方法,针对于某个对象调用。)
      3.可以通过对象引用或类名(不需要实例化)访问静态成员。

    看一段小程序:

    public class Cat{
        private static int sid = 0;
        private String name;
        int id;
        Cat(String name){
            this.name = name;
            id = sid++;
        }
    
        public void info(){
            System.out.println("My Name is "+ name + "No."+ id);
        }
    
        public static void main(String[] args){
            Cat.sid = 100;
            Cat mimi = new Cat("mimi");
            Cat pipi = new Cat("pipi");
    
            mimi.info();
            pipi.info();
        }
    }
    

    静态的成员变量放在数据区(data seg),当有一个Cat被new出来,数据区中多了一个静态的成员变量sid。静态变量只有一份,存在于数据区,id和name是非静态变量,每个cat都有一份。静态变量是属于某个类的,它不属于单独的某个对象。如何访问静态的对象呢?任何一个该类的对象都可以访问,访问的是同一块内存,没有对象同样可以访问,类名.静态变量即可访问。

    创建Cat对象时的图示如下: image.png

    这里要注意的是:字符串常量位于数据区。

    当Cat创建完成,构造方法调用完毕,为方法分配的形参全部消失,sid的值变为101,此时图如下所示: image.png 这里需要注意的是sid++; ++在后,是先用后加,因此,sid的值先赋给id,再+1,变为101.

    继承

    Java中使用extends关键字来实现类的继承机制,通过基础,子类拥有了父类中所有成员变量和方法。Java只支持单继承,不支持多继承(一个子类只能有一个父类,一个父类可以派生出多个子类)。

    从内存分析继承:看一个小程序

    class Person{
        private String name;
        private int age;
        public void setName(String name){this.name = name;}
        public String getName(){return name;}
        public void setAge(String age){this.age = age;}
        public int getAge(){return age;}
    }
    class Student extends Person{
        private String school;
        public String getSchool(){return school;}
        public void setSchool(String school){this.school = school;}
    }
    public class TestPerson{
        public static void main(String[] args){
            Student student = new Student();
            student.setName("John");
            student.setAge(18);
            student.setSchool("SCh");
            System.out.println(student.getName());
            System.out.println(student.getAge());
            System.out.println(student.getSchool());
        }
    }
    
    图解继承
    访问控制
    Java的权限修饰符位于类的成员定义前,用来限定其他对象对该类成员对象的访问权限。 image.png

    注意:1.对于class的权限修饰只可以用public和default。
    2.public类可以在任意地方被访问。
    3.default类只可以被同一个包内部的类访问。

    重写:

    1.在子类中可以根据需要对从基类中继承的方法进行重写。
    2.重写方法必须和被重写方法具有相同的方法名称,参数列表和返回类型。
    3.重写方法不能使用比被重写方法更严格的访问权限。

    super关键字

    在Java中使用super来引用基类的成分。
    看一段小程序:

    class FatherClass{
        public int value;
        public void f() {
            value = 100;
            System.out.println("FatherClass Value : "+ value);
        }
    }
    
    class ChildClass extends FatherClass{
        public int value;
        public void f() {
            super.f();
            value = 200;
            System.out.println("ChildClass Value:"+value);
            System.out.println("父类value:"+super.value);
        }
    }
    
    public class demo {
        
        public static void main(String[] args) {
            ChildClass cc = new ChildClass();
            cc.f();
        }
    }
    
    程序执行完成后,内存中的图解如下: super关键字
    继承中的构造方法

    1.子类的构造过程中必须调用其基类的构造方法。
    2.子类可以在自己的构造方法中使用super(argument_list)调用基类的构造方法。(使用this(argument_list)调用本类中另外的构造方法)
    3.如果子类的构造方法中没有显示的调用基类构造方法,则系统默认调用基类无参数的构造方法。
    4.如果子类构造方法中既没有显示调用基类构造方法,而基类中又没有无参的构造方法,则编译出错。
    5.如果调用super,必须写在子类构造方法的第一行。

    Object类

    Object类是Java类的根基类,如果在类的声明中未使用extends关键字指明其基类,则默认基类为Object类。

    toString():1.描述当前对象的有关信息。
    2.在进行String与其他类型数据的连接操作时,如"value :" + value,将自动调用该对象类的toString()方法。
    3.toString()可重写。

    equals方法

    Api中是这样解释Object类中的equals方法的:

    指示其他某个对象是否与此对象“相等”。
    equals 方法在非空对象引用上实现相等关系:  
    
        1. 自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。
        2. 对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。
        3. 传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。
        4. 一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。
        5.对于任何非空引用值 x,x.equals(null) 都应返回 false。
    
        Object 类的 equals 方法实现对象上差别可能性最大的相等关系;
        即,对于任何非空引用值 x 和 y,当且仅当 x 和 y 引用同一个对象时,
        此方法才返回 true(x == y 具有值 true)。 
    
        注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,
        该协定声明相等对象必须具有相等的哈希码。 
    
    

    总结下来就是:
    1.Object类的equals方法定义为:x.equals(y)当x和y是同一个对象的引用时返回true,否则返回false。(对比:与“ == ” 相同)
    2.Java中的某些类,如String,Date等,重写了Object的equals方法,调用这些类的equals方法,x.equals(y),当x和y所引用的对象是同一类对象且属性内容相等时返回true,否则返回false。对于各个类重写的equals()可翻看源码查看具体比较的是什么。
    3.equals()可重写。

    hashCode()
    对于hashCode在Api中的解释如下:
    hashCode 的常规协定是: 
    *   在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,
        必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。
        从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
    *   如果根据equals(Object) 方法,两个对象是相等的,
        那么对这两个对象中的每个对象调用 hashCode方法都必须生成相同的整数结果。
    *   如果根据equals(java.lang.Object)方法,两个对象不相等,那么对这两个对象中的任一对象上调用
         hashCode方法不要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。
    
        实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。
       (这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。) 
    

    在进行两个对象比较时,有时使用equals()有时使用hashCode(),两个相等的对象必须具有相同的hashcode值,也就是所谓的地址(不是纯粹的物理地址)。在下一章容器中会讲到,当比较Map中的键时,一般使用hashCode()来比较,hashCode()效率更高。

    哈希编码: 在内存中的每个对象,都是通过它自己独有的哈希编码找到的。在栈内存中存储的,也就是我们前面所说的指向对象的地址。
    对象转型

    1.一个父类的引用类型变量可以指向其子类对象。
    2.一个父类引用不可以访问其子类对象新增加的成员(属性和方法)
    3.可以使用 变量 instanceof 类名 来判断该引用型变量所指向的对象是否属于该类或者该类的子类。
    4.子类的对象可以当作父类的对象来使用,称作向上转型,反之称作向下转型。

    父类对象的引用指向子类对象,叫做向上转型。反之叫做向下转型。
    看这样一个小程序:

    class Animal{
        
        public String name;
         Animal(String name) {
            this.name = name;
        }
    }
    
    class Cat extends Animal{
        public String eyesColor;
         Cat(String n,String c) {
            super(n);
            eyesColor = c;
        }
    }
    
    
    class Dog extends Animal{
        public String furColor;
         Dog(String n,String c) {
             super(n);
             furColor = c;  
        }
    }
    public class Test1 {
    
        public static void main(String[] args) {
            Animal a = new Animal("name");
            Cat c = new Cat("catName","blue");
            Dog d = new Dog("dogName","black");
            
            System.out.println(a instanceof Animal);
            System.out.println(c instanceof Animal);
            System.out.println(d instanceof Animal);
            System.out.println(a instanceof Cat);
            
            a = new Dog("bigYellow","yellow");
            System.out.println(a.name);
            System.out.println(a instanceof Animal);
            System.out.println(a instanceof Dog);
            
            Dog d1 = (Dog)a;
            System.out.println(d1.furColor);        
    
        }
    
    }
    
    当程序运行到a = new Dog("bigYellow","yellow"); 时,此时父类引用指向了子类的对象,在内存中是这样的: 向上转型

    也可以这样理解:a只看得到Dog中的Animal部分,当把a强制转换成Dog类型之后,a才能看到整个Dog部分,才可以访问到Dog中的furColor属性。

    多态(动态绑定)

    动态绑定是指在执行期间(而非编译期间)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。

    看一段代码:

    class Animal{
        private String name;
        Animal(String name){
            this.name = name;
        }
    
        public void enjoy(){
            System.out.println("roal......");
        }
    }
    
    
    class Cat extends Animal{
        private String eyesColor;
        Cat(String name,String eyesColor){
            super(name);
            this.eyesColor = eyesColor;
        }
    
        public void enjoy(){
            System.out.println("cat roal.....");
        }
    
    
    }
    
    class Dog extends Animal{
        private String furColor;
        Dog(String name,String furColor){
            super(name);
            this.furColor = furColor;
        }
    
      
        public void enjoy(){
            System.out.println("dog roal......");
        }
    }
    
    class Lady{
        private String name;
        private Animal pet;
        Lady(String name,Animal pet){
            this.name = name;
            this.pet = pet;
        }
    
        public void myPetEnjoy(){
            pet.enjoy();
        }
    }
    
    
    public class Test1{
        public static void main(String[] args) {
            Cat c = new Cat("catName","blue");
            Dog d = new Dog("dogName","black");
            Lady l1 = new Lady("l1",c);
            Lady l2 = new Lady("l2",d);
            l1.myPetEnjoy();
            l2.myPetEnjoy();
        }
    }
    
    打印出的结果为: 图片.png l1中的pet,new出的实际是Cat类型的对象,而l2中的pet,new出的实际是Dog类型的对象,所以调用enjoy()分别是Cat中的enjoy和Dog中的enjoy。具体内存图解如下: 图片.png
    图片.png

    所谓动态绑定,就是运行时new出什么对象,就调用该对象中的方法。
    实现多态需要有三个条件:
    1.要有继承。
    2.要有重写。
    3.父类引用指向子类对象。
    当这三个条件满足之后,当调用父类中被重写的方法时,实际中new的是哪个子类对象,就调用哪个子类对象中的该方法。

    抽象类

    用absetract关键字修饰的类和方法分别叫做抽象类和抽象方法。含有抽象方法的类必须被声明为抽象类,抽象类必须被继承,抽象方法必须被重写。抽象类不能被实例化,抽象方法只需声明,不需实现。

    final关键字

    final可以用来修饰变量,方法,类。用final修饰的变量值不能被改变(可以理解成final的值是只读的),方法不能被重写,类不能被继承。

    接口(interface)

    接口是抽象方法和常量值的定义的集合。
    从本质上讲,接口是一种特殊的抽象类,这种抽象类中只包含常量和方法的定义。

    public interface Runner {
        public static final int id = 1;
        //等同于这句话
        int id = 1;
    
        void start();
        void run();
        void stop();
    }
    

    接口的特性:
    1.接口可以多重实现。
    2.接口中声明的属性默认为public static flnal的,也只能是public static flnal的。
    3.接口中只能定义抽象方法,而且这些方法默认为public的,也只能是public的。
    4.接口可以继承其它接口,并添加新的属性和抽象方法。

    看下面这段小程序:

    interface Singer{
         void sing();
         void sleep();
    }
    
    interface Printer{
        void paint();
        void eat();
    }
    
    class Student implements Singer{
        private String name;
        Student(String name){
            this.name = name;
        }
    
       @Override
       public void sing() {
           System.out.println("student is singing......");
       }
    
        /**
         * @return the name
         */
        public String getName() {
            return name;
        }
    
    
        public void study() {
            System.out.println("Studying....");
        }
    
        @Override
        public void sleep() {
            System.out.println("student is sleeping.....");
            
        }
    }
    
    
    class Teacher implements Singer,Printer{
        private String name;
            /**
             * @return the name
             */
            public String getName() {
                return name;
            }
    
            Teacher(String name){
                this.name = name;
            }
    
            public void teach() {
                System.out.println("teaching");
            }
    
            @Override
            public void sing() {
                System.out.println("teacher is singing....");
            }
    
            @Override
            public void sleep() {
                System.out.println("teacher is sleeping.....");
            }
    
            @Override
            public void paint() {
                System.out.println("teacher is painting......");
            }
    
            @Override
            public void eat() {
                System.out.println("teacher is eating....");
            }
    
    }
    
    /**
     * Test
     */
    public class Test {
    
        public static void main(String[] args) {
            Singer s1 = new Student("le");
            s1.sing();
            s1.sleep();
    
            Singer s2 = new Teacher("steven");
            s2.sing();
            s2.sleep();
    
    
            Printer p1 = (Printer)s2;
            p1.paint();
            p1.eat();
        }
    }
    

    对这句代码的解释:

           Singer s1 = new Student("le");
            s1.sing();
            s1.sleep();
    
    图片.png 图片.png

    第二句:

            Singer s2 = new Teacher("steven");
            s2.sing();
            s2.sleep();
    
    
            Printer p1 = (Printer)s2;
            p1.paint();
            p1.eat();
    

    这段代码的前半部分跟上面是相同的,s2只能看到sing()和sleep(),调用的是Teacher的sing()和sleep(),当s2被强制转化成Printer对象之后,只看得到Printer中的方法,而此时new出的是Teacher对象,调用的就是Teacher的paint()和eat().

    至此,面向对象的知识点基本上是过完了。
    下一章,讲异常。

    易出错问题总结:

    1.形参列表在内存中的分配情况:
    形参也是一种局部变量,是专门为某个方法分配的局部变量,当方法执行时,在栈内存中临时分配一小块空间,用来存储它。当方法执行结束,为方法分配的局部变量在内存中消失,为其分配的这块内存被释放。构造方法的形参也是如此。
    2.方法存在于内存中的代码区(code seg),方法名指向存储该段代码的代码区。

    3.String类型的变量,在内存中的分布情况如下:String类型的变量的值位于内存中的数据区(data seg). 图片.png 如果要讲String的值赋给另一个String,只是将name中的地址赋值给了另一个String而已。 4.static类型的变量,一个类中只有一份,位于数据区(data seg)。 图片.png

    5.方法的返回值,也是在栈内存中临时创建一块内存,将返回的值赋值给它。

    欢迎关注个人公众号,加入进来一起学习吧!


    平头哥写代码

    相关文章

      网友评论

          本文标题:Java面向对象

          本文链接:https://www.haomeiwen.com/subject/vibewftx.html