Java多态

面向对象的三大特性:封装、继承、多态。

那么多态是什么,有什么优点,又是怎么实现的呢?

(1) 什么是多态

多态是同一个行为具有多个不同表现形式或形态的能力。

比如:猫、狗 都是动物,但是在这个行为上,表现不一样,猫叫起来是,狗叫起来是

class Animal {
    public void say() {
        System.out.println("叫");
    }
}

class Dog extends Animal {
    public void say() {
        System.out.println("汪汪");
    }
}

class Cat extends Animal {
    public void say() {
        System.out.println("喵喵");
    }
}
public static void main(String[] args) {
    Animal animal = new Dog();
    animal.say();
    Animal animal2 = new Cat();
    animal2.say();
}

(1.1) 多态的优点

  1. 消除类型之间的耦合关系
  2. 可替换性
  3. 可扩充性
  4. 接口性
  5. 灵活性
  6. 简化性

(1.2) 多态实现的必要条件

  1. 继承
  2. 重写
  3. 父类引用指向子类对象。

(2) 多态使用

多态的实现途径有三种:重写、重载、接口。
虽然它们的实现方式不一样,但是核心都是:同一行为的不同表现形式。

(2.1) 继承 (运行时多态)

子类继承父类 重写父类方法

// java.io.InputStream 
public abstract class InputStream {

    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

}

// java.io.FileInputStream
public class FileInputStream extends InputStream {

    @Override
    public int read(byte b[]) throws IOException {
        return readBytes(b, 0, b.length);
    }

    private native int readBytes(byte b[], int off, int len) throws IOException;

}

// java.io.ByteArrayInputStream
public class ByteArrayInputStream extends InputStream {

    @Override
    public synchronized int read(byte b[], int off, int len) {
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        }

        if (pos >= count) {
            return -1;
        }

        int avail = count - pos;
        if (len > avail) {
            len = avail;
        }
        if (len <= 0) {
            return 0;
        }
        System.arraycopy(buf, pos, b, off, len);
        pos += len;
        return len;
    }
}
public static void main(String[] args) {
    String filePath = "/tmp/test.txt";  
    InputStream inputStream = new FileInputStream(filePath);
    byte[] arr = {1, 2};
    inputStream.read(arr);
}

(2.2) 接口 (运行时多态)

// java.util.List
public interface List<E> {

  boolean add(E e);

}

// java.util.ArrayList
public class ArrayList<E>  implements List<E> {

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

}

// java.util.LinkedList
public class LinkedList<E> implements List<E> {

    public boolean add(E e) {
        linkLast(e);
        return true;
    }
}
public static void main(String[] args) {
    List list = new ArrayList<>();
    list.add(1);
}

(2.3) 重载 (编译时多态)

// java.lang.String 
public final class String {

    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0


    public String() {
        this.value = "".value;
    }

    public String(byte bytes[]) {
        this(bytes, 0, bytes.length);
    }

    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }

    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

    public String(StringBuffer buffer) {
        synchronized(buffer) {
            this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
        }
    }

}
public static void main(String[] args) {

    String s = new String("abc");

    char[] arr = {'a' ,'b'};
    String s2 = new String(arr);

}

 

(3) 多态的实现原理

多态分两种:
编译时多态(又称静态多态)
运行时多态(又称动态多态)

 
重载(overload)就是编译时多态的一个例子,编译时多态在编译时就已经确定,运行时运行的时候调用的是确定的方法。

(3.1) 运行时多态

 我们通常所说的多态指的都是运行时多态,也就是编译时不确定究竟调用哪个具体方法,一直延迟到运行时才能确定。这也是为什么有时候多态方法又被称为延迟方法的原因。

在编译期并不知道 animal 这个引用到底指向哪个实例对象,所以编译期无法进行绑定,必须等到运行期才能确切知道最终调用哪个子类的 say()方法,这便是动态绑定。

多态的底层实现是动态绑定,即在运行时才把方法调用与方法实现关联起来。

(3.2) 运行时类型判定(Run-Time Type Identification)

多态实现的技术基础是 RTTI,即 Run-Time Type Identification(运行时类型判定),它的作用是在我们不知道某个对象的确切的类型信息时(即某个对象是哪个类的实例),可以通过 RTTI 相关的机制帮助我们在编译时获取对象的类型信息。

而 RTTI 的功能主要是通过 Class 类文件实现的,更精确一点来说,是通过 Class 类文件的方法表实现的。

JVM内存结构

虚拟机栈中会存放当前方法调用的栈帧,在栈帧中,存储着局部变量表、操作栈、动态连接、返回地址和其他附加信息。
多态的实现过程,就是方法调用动态分派的过程,通过栈帧的信息去找到被调用方法的具体实现,然后使用这个具体实现的直接引用完成方法调用。

(3.3) Java的方法调用方式

Java的方法调用有两类,动态方法调用静态方法调用
JVM 的方法调用指令有四个,分别是 invokestatic,invokespecial,invokesvirtual 和 invokeinterface。前两个是静态绑定,后两个是动态绑定的。

JVM方法调用指令 作用 绑定类型
invokestatic 调用静态方法 静态绑定
invokespecial 调用实例构造器方法、私有方法和父类方法 静态绑定
invokevirtual 调用虚方法 动态绑定
invokeinterface 调用接口方法,运行时确定具体实现 动态绑定
invokedynamic 运行时动态解析所引用的方法,然后再执行,用于支持动态类型语言

虚方法的方法调用与方法实现的关联(也就是分派)有两种,
一种是在编译期确定,被称为静态分派,比如方法的重载;
一种是在运行时确定,被称为动态分派,比如方法的覆盖。对象方法基本上都是虚方法。

静态绑定在编译期就已经确定,这是因为静态方法、构造器方法、私有方法和父类方法可以唯一确定。这些方法的符号引用在类加载的解析阶段就会解析成直接引用。因此这些方法也被称为非虚方法

特别说明:final 方法由于不能被覆盖,可以唯一确定,因此 Java 语言规范规定 final 方法属于非虚方法,但仍然使用 invokevirtual 指令调用。

(3.4) 运行时多态实现原理-继承

继承:在执行某个方法时,在方法区中找到该类的方法表,再确认该方法在方法表中的偏移量,找到该方法后如果被重写则直接调用,否则认为没有重写父类该方法,这时会按照继承关系搜索父类的方法表中该偏移量对应的方法。

java多态-父类方法表
java多态-子类方法表

(3.5) 运行时多态实现原理-接口

接口:Java 允许一个类实现多个接口,从某种意义上来说相当于多继承,这样同一个接口的的方法在不同类方法表中的位置就可能不一样了。所以不能通过偏移量的方法,而是通过搜索完整的方法表。

每次接口调用都要搜索方法表,所以从效率上来说,接口方法的调用总是慢于类方法的调用的。

参考资料

[1] Java多态的实现机制是什么,写得非常好!
[2] Java技术——多态的实现原理
[3] Java多态的底层原理
[4] 从jvm虚拟机角度看Java多态 ->(重写override)的实现原理