【JavaSE】你真的了解Java多态吗

291 阅读10分钟

1. 理解

多态,翻译为:polymorphism。该单词由poly+morphism组成,poly表示许多,morphism表示状态。

多态字面上指的是对象的多种形态。

2. 现象

现实事物经常会体现出多种形态,如学生,学生是人的一种,则一个具体的同学张三既是学生也是人,即出现两种形态。

使用Java代码可以表示成:

Person zhangsan = new Student();

3. 前提

  1. 必须有子类和父类,具有继承或实现。
  2. 子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
  3. 父类的引用指向子类的对象。

4. 优缺点

优点:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。

缺点:利用多态无法访问子类所特有的方法。

5. 特性

5.1 向上转型

5.1.1 定义

父类型引用指向子类型对象,这种属于自动转换

5.1.2 案例

// Dog类继承了Animal类
Animal animal = new Dog();

向上转型时,父类引用只能调用父类原定义的方法或者子类重写后的方法,而子类中的独有方法则是无法调用的。

5.2 动态绑定

5.2.1 引入

见如下代码:

// 父类
public class Animal {

    public void whoop() {
        System.out.println("I am an animal");
    }
}

// 子类Dog
public class Dog extends Animal {

    @Override
    public void whoop() {
        System.out.println("I am a dog");
    }
}

// 子类Cat
public class Cat extends Animal {

    @Override
    public void whoop() {
        System.out.println("I am a cat");
    }
}

// 测试类
public class Test {

    public static void whoopTest(Animal animal) {
        animal.whoop();
    }

    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();
        // 执行的是Dog类中的whoop方法
        whoopTest(dog);
        // 执行的是Cat类中的whoop方法
        whoopTest(cat);
    }
}

我们在IDEA中按住Ctrl + 左击进入Test类中whoopTest方法中的whoop方法的调用,发现该编译时链接的是Animal类中定义的whoop方法。但是为什么当我传入的参数的真实类型是Dog时,运行时就会自动去调用Dog类中定义的whoop方法呢?当我传入的参数的真实类型是Cat时,运行时就会自动取调用Cat类中定义的whoop方法呢?动态绑定。

5.2.2 定义

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

6. 向下转型

6.1 定义

定义:子类型引用指向父类型引用所指向的子类型对象,这种属于强制转换

6.2 案例

如果父类引用想调用子类中独有的方法,那么只有通过向下转型来调用。

Animal animal = new Dog();
// 向下转型
Dog dog = (Dog) animal;
// Dog类独有的方法
dog.whoop();

注意:向下转型的父类型引用真实指向的对象必须和目标子类的类型一样,否则会报ClassCastException

所以向下转型是不安全的,因为我们有时候无法确定该父类型引用真实指向的对象的类型是否和目标子类的类型一样。所以我们需要使用instanceof关键字。

上述例子中,由于已知父类引用指向的目标子类的类型是什么,所以向下转型不会出错。

6.3 instanceof关键字

当不知道父类引用指向的目标子类的类型的情况下,需要使用instanceof加以判断

// 判断animal引用指向的对象是不是Dog类型对象
if  (animal instanceof Dog) {
    // 向下转型
    Dog dog = (Dog) animal;
    // Dog类独有的方法
    dog.test();
}

7. 静态绑定

7.1 定义

编译阶段能够确定方法在内存什么位置的机制就叫静态绑定机制。

static方法,final方法,private方法都遵循静态绑定机制。

7.2 案例

见以下代码:

public class Util {

    public static int add(int a, int b) {
        return a + b;
    }
}

public class Test {

    public static void main(String[] args) {
        int result = Util.add(3, 2);
    }
}

当main方法被编译时,编译器就会把Util.add方法的相关信息(包名,类名,方法名,返回值类型等)传入Test类常量池中的某个常量表中(假设为a常量表)。在JVM第一次运行该方法时,会进行一次常量池解析,目的是通过编译器存储的相关信息去方法区中找到该方法的直接地址,然后将该直接地址存入a常量表。等到JVM第二次或者多次运行该方法时,就可以直接从a常量表中存储的直接地址找到该方法的字节码完成调用。

8. 方法重写(补充)

方法重写有如下规定:

  • 子类中重写方法的方法名,形参列表,返回值类型必须和父类中被重写方法一致。
  • 子类中重写方法抛出异常的范围不能比父类中被重写方法的更广泛。
  • 子类中重写方法的权限修饰符的权限不能比父类中被重写方法的更低。

在多态中,正是因为重写方法的返回值类型也可以向上转型,所以需要增加一条规定:

  • 子类中重写方法的返回值类型可以是父类中被重写方法的返回值类型的子类

9. 多态底层原理

9.1 Java方法调用

Java 的方法调用有两类,动态方法调用与静态方法调用。

  • 静态方法调用:是在编译时就已经确定好具体调用方法的情况。
  • 动态方法调用:是在调用的时候才确定具体的调用方法,这就是动态绑定,也是多态要解决的核心问题。

JVM 的方法调用指令有四个,分别是 invokestaticinvokespecialinvokevirtualinvokeinterface。前两个是静态绑定,后两个是动态绑定。

9.2 两种动态调用

Java 对于方法调用动态绑定的实现主要依赖于方法表。但是这里分两种调用方式,分别是类引用调用invokevirtual和接口引用调用invokeinterface。两者的实现有所不同。

从性能上来讲,类引用调用的性能更高 。因为invokevirtual是基于偏移量的方式来查找方法的,而invokeinterface是基于搜索的。

类引用调用只需要修改方法表的指针就可以实现动态绑定(具有相同签名的方法,在父类、子类的方法表中具有相同的索引号)。

接口引用调用需要扫描整个方法表才能实现动态绑定(因为,一个类可能实现多个接口,另外一个类可能只实现一个接口,无法具有相同的索引号)。

9.3 类引用调用

类引用调用的大致过程为:

在编译过程中,Java编译器会将每个类的源代码编译成一个个class字节码,同时将该类调用方法的符号引用一同写入对应class文件中。在执行过程中,JVM根据class文件找到调用方法的符号引用,然后在该类的方法表上找到偏移量,最后根据this指针确定方法的绝对地址。如果在方法表中找到该方法,则直接调用。如果没找到,JVM会认为没有重写父类该方法,就会按照继承关系,从父类往子类依次搜索方法表。

JVM结构图:

image.png

从上图可以看出,在程序运行期,当需要使用到某个类时,类加载子系统会将对应的class文件加载进入JVM,并在内部建立该类的类型信息(类型信息就是JVM存储class文件的一种数据结构),包含类定义的所有信息(方法代码,静态变量,成员变量,方法表)。所有类型信息都存储在方法区。

注意

  1. 方法区中的类型信息和堆中的class对象是不同的。方法区中的类型信息只有唯一的实例,而堆中可以有多个该class对象。可以通过堆中的class对象访问到方法区中对应的类型信息。类似于Java的反射机制,可以通过class对象访问到该类的所有信息一样。

  2. 方法区和堆空间一样,是各个线程共享的内存区域。用于存储已被虚拟机加载的类信息,常量,静态变量,编译器编译后的代码等。

  3. 运行时常量池是方法区的一部分,class文件中除了有类信息之外,还有一项信息是常量池。用于存放编译器生成的各种符号引用,这部分信息在类加载时进入方法去的运行时常量池中。

  4. 方法区的内存回收目标是针对常量池的回收和对类型信息的卸载。

案例

// 父类
public class Animal {

    public void whoop() {
        System.out.println("I am an animal");
    }

    public void speak() {
        System.out.println("Animal is speaking");
    }

    @Override
    public String toString() {
        return "This is an animal";
    }
}


// 子类
public class Dog extends Animal {

    // 自定义方法
    public void eat() {
        System.out.println("Dog is eating");
    }
    
    // 重写父类方法
    @Override
    public void speak() {
        System.out.println("Dog is speaking");
    }

    // 重写Object类方法
    @Override
    public String toString() {
        return "This is a dog";
    }
}


// 子类
public class Cat extends Animal {

    // 自定义方法
    public void sing() {
        System.out.println("Cat is singing");
    }

    // 重写父类方法
    @Override
    public void speak() {
        System.out.println("Cat is speaking");
    }

    // 重写Object类方法
    @Override
    public String toString() {
        return "This is a cat";
    }
}

当三个类的class文件被加载到JVM后,JVM方法区就包含各自类的信息。

方法区占用如下图所示:

image.png

上图可以清楚看到调用方法的指针指向,而且可以看出相同签名的方法在方法表中的偏移量是一样的。这个偏移量只是说Dog方法表中的继承自Object类的方法、继承自Animal类的方法的偏移量与Animal类中的相同方法的偏移量是一样的,与Cat是没有任何关系的。

Dog和Cat的方法表中,包含继承自Object的方法,继承自直接父类Animal的方法,和自定义的方法。注意方法条目指向的具体方法地址,如Dog方法表中继承自Object的方法中,只有toString()方法指向了自己的实现(Dog的方法代码),其余皆指向Object的方法代码。whoop()方法指向父类的实现(Animal的方法代码)speak方法指向自己的实现,eat()方法也指向自己的实现。

这就解释了为什么在多态中,如果子类重写了父类的方法,真正调用的是子类的方法。如果子类没有重写父类的方法,真正调用的是父类的方法。

10. 扩展

见以下代码:

// 父类
public class Animal {

    protected String name = "animal";
}

// 子类
public class Dog extends Animal {

    protected String name = "dog";
}

// 子类
public class Cat extends Animal {

    protected String name = "cat";
}

public class Test {

    public static void main(String[] args) {
        Animal dog = new Dog();
        // animal
        System.out.println(dog.name);
        Animal cat = new Cat();
        // animal
        System.out.println(cat.name);
    }
}

为什么父类引用无论指向的真实子对象是谁,输出的都是父类的成员变量?

:在Java中只有方法才有重写,所以成员变量是不具备多态性的。dog虽然指向的是Dog实例,但是dog是Animal类型的,所以Dog实例中的name表现出来的是Animal的特性,因此输出的是"animal",cat同理。