Skip to content

Latest commit

 

History

History
255 lines (186 loc) · 9.55 KB

对象特性.md

File metadata and controls

255 lines (186 loc) · 9.55 KB

对象特性

封装

封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法

class Person{
    String name; // 属性
    int age;
    
    // 方法
    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }
}

继承

继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。

class Student extends Person{
    double grade; // 在已经的name,age中, 在学生类中添加了成绩或者班级
    
    // 方法
    public Student(String name, int age, double grade) {
        this.name = name;
        this.age = age;
        this.grade = grade;
    }
}

多态

定义

三要素:加黑的地方!

首先我觉得即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。强调在编译的时候,不知道该引用指向的是哪个对象的实例,包括调用哪个实例的方法,只有运行的时候,动态知道。

举个例子:

任何事物的多个姿态,多个形态。比如,你说一个猫在吃东西,同样的,你也能说一个动物在吃东西。

public class Test {
    public static void main(String[] args){
        Animal animal = new Cat();
        animal.eat() // 猫也会吃饭
        // 你看到了一只猫,同样它也是动物
        // 比如有很多其他种类继承了动物哈,
        // 当编译期间的animal引用变量,到底指的哪个实例对象,(重要)(主语是引用变量)
        // 或者该引用调用的eat方法,到底是哪个实例对象的eat,编译期间恐怕不知道哦(主语是引用变量)
        // 只有运行期间,哦哦, 原来是猫的eat方法哇...
    }
}

表现形式

  • Java的方法重载,就是在类中可以创建多个方法,它们具有相同的名字,但可具有不同的参数列表、返回值类型。调用方法时通过传递的参数类型来决定具体使用哪个方法,这就是多态性。比如,java lang的包很多工具类如String工具类,那么就有很多相同的名字,但是参数类型、数量和返回值等等不一样。

  • Java的方法重写,是父类与子类之间的多态性,子类可继承父类中的方法,但有时子类并不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写。重写的参数列表和返回类型均不可修改。这也是多态性。比如JUC的AQS框架,凡事继承了AQS的那几个类,其中几个重要的方法,都被重写了,很多这样的情况。

底层

首先要说:首先当程序运行需要某个类时,类加载器会将相应的class文件载入到JVM中,并在方法区建立该类的类型信息(包括方法代码,类变量、成员变量以及方法表。(标黑的这个玩意)

面试官:方法表有啥?

我:方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。

接着回答:方法表是实现动态调用的核心。为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向记录该类方法的方法表,方法表中的每一个项都是对应方法的指针

到这里:就要分情况讨论了,一个是方法调用,一个是接口

方法调用
class Person {
    // 重写object的toString
    public String toString(){
        return "I'm a person.";
    }
    public void eat(){}
    public void speak(){}

}

class Boy extends Person{
    // 重写object的toString
    public String toString(){
        return "I'm a boy";
    }
    // 继承Person的speak
    public void speak(){}
    // 自己实现的自定义方法
    public void fight(){}
}

class Girl extends Person{
    // 重写object的toString
    public String toString(){
        return "I'm a girl";
    }
    // 继承Person的speak
    public void speak(){}
    // 自己实现的自定义方法
    public void sing(){}
}

多态方法调用-I0jtEe

这张图的指向:你可以根据颜色对应上,注意方法表条目指向的具体的方法地址。其次注意蓝色部分其继承自于 Person 的方法 eat() 和 speak() 分别指向 Person 的方法实现和本身的实现。如果子类改写了父类的方法,那么子类和父类的那些同名的方法共享一个方法表项。因此,所有继承父类的子类的方法表中,其父类所定义的方法的偏移量也总是一个定值。Person 或 Object中的任意一个方法,在它们的方法表和其子类 Girl 和 Boy 的方法表中的位置 (index) 是一样的。这样 JVM 在调用实例方法其实只需要指定调用方法表中的第几个方法即可。

调用过程:

  1. 在常量池里找到方法调用的符号引用(肯定先看到Person定义引用类型)
  2. 查看Person的方法表,得到speak方法在该方法表的偏移量(假设为15),这样就得到该方法的直接引用。
  3. 根据this(invoker this字节码)指针得到具体的对象(即 girl 所指向的位于堆中的对象)。
  4. 根据对象得到该对象对应的方法表,根据偏移量15查看有无重写(override)该方法,如果重写,则可以直接调用(Girl的方法表的speak项指向自身的方法而非父类);如果没有重写,则需要拿到按照继承关系从下往上的基类(这里是Person类)的方法表,同样按照这个偏移量15查看有无该方法。

接口调用

一个类可以实现多个接口,那么就像多继承一样,这样的话,在方法表中的索引就会不一样,所以Java 对于接口方法的调用是采用搜索方法表的方式。

初始化顺序

有这样的两个类:Person和Student

Person:

class Person{
    // 静态代码块
    static {
        System.out.println("Person 静态方法");
    }
    // 代码块
    {
        System.out.println("Person 代码块");
    }
    // 构造方法
    Person(){
        System.out.println("Person 构造方法");
    }
}

Student:

class Student extends Person{
    // 静态代码块
    static {
        System.out.println("Student 静态方法");
    }
    // 代码块
    {
        System.out.println("Student 代码块");
    }
    // 构造方法
    Student(){
        System.out.println("Student 构造方法");
    }
}

测试

public class Main {
    public static void main(String[] args) {
        Student student = new Student();
    }
}

// result:

Person 静态方法
Student 静态方法
Person 代码块
Person 构造方法
Student 代码块
Student 构造方法

解释:还不是因为类加载器的双亲委派哈, 先走类方法,也就是说static属于类,所以先调用Person static -> Student static,接着走对象的初始化,那么对象初始化了,还是先走父类的初始化了,所以Perosn {} -> Person(){},最后走子类的初始化Student {} -> Student(){}

接口和抽象类的区别

  1. 方法是否能实现:所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法
  2. 变量:接口中除了static、final变量,不能有其他变量,而抽象类中则不一定。
  3. 实现:一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过implement关键字扩展多个接口。
  4. 修饰符:接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。 5

匿名内部类传参数为什么需要final

public class Main {
    public static void main(String[] args) {
        String str = "m";
        new Thread(){
            @Override
            public void run() {
                System.out.println(str);
            }
        }.start();
    }
}
// 在1.8版本之前是编译不通过的,1.8能编译能过,但是还是需要final保证局部变量的数据一致性

反编译

 
public class Hello$1 extends Thread {
	
	private String val$str;
	
	Hello$1(String paramString) {
		this.val$str = paramString;
	}
 
	public void run() {
		System.out.println(this.val$str);
	}
 
}

解释:局部变量是被当成了参数传递给匿名对象的构造器(那就相当于拷贝一份,那就是浅拷贝了),这样的话,如果不管是基本类型还是引用类型,一旦这个局部变量是消失(局部变量随着方法的出栈而消失),还是数据被改变,那么此时匿名构造类是不知道的,毕竟是你浅拷贝了一份,那么如果加上final,这个数据或者引用永远都不会改变,保证数据一致性。注意:在JDK8中如果我们在匿名内部类中需要访问局部变量,那么这个局部变量不需要用final修饰符修饰。

四种修饰符的限制范围

  1. public:可以被所有其他类所访问。

  2. private:只能被自己访问和修改。

  3. protected:自身,子类及同一个包中类可以访问。

  4. default(默认):同一包中的类可以访问