Java面向对象

2018-09-01  本文已影响36人  kim_liu

一、类和对象

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

类与类之间的关系:
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.方法的返回值,也是在栈内存中临时创建一块内存,将返回的值赋值给它。

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


平头哥写代码
上一篇下一篇

猜你喜欢

热点阅读