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) 多态实现的必要条件
- 继承
- 重写
- 父类引用指向子类对象。
(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 类文件的方法表实现的。
虚拟机栈中会存放当前方法调用的栈帧,在栈帧中,存储着局部变量表、操作栈、动态连接、返回地址和其他附加信息。
多态的实现过程,就是方法调用动态分派的过程,通过栈帧的信息去找到被调用方法的具体实现,然后使用这个具体实现的直接引用完成方法调用。
(3.3) Java的方法调用方式
Java的方法调用有两类,动态方法调用
与静态方法调用
。
JVM 的方法调用指令有四个,分别是 invokestatic,invokespecial,invokesvirtual 和 invokeinterface。前两个是静态绑定,后两个是动态绑定的。
JVM方法调用指令 | 作用 | 绑定类型 |
---|---|---|
invokestatic | 调用静态方法 | 静态绑定 |
invokespecial | 调用实例构造器 |
静态绑定 |
invokevirtual | 调用虚方法 | 动态绑定 |
invokeinterface | 调用接口方法,运行时确定具体实现 | 动态绑定 |
invokedynamic | 运行时动态解析所引用的方法,然后再执行,用于支持动态类型语言 |
虚方法的方法调用与方法实现的关联(也就是分派)有两种,
一种是在编译期确定,被称为静态分派,比如方法的重载;
一种是在运行时确定,被称为动态分派,比如方法的覆盖。对象方法基本上都是虚方法。
静态绑定在编译期就已经确定,这是因为静态方法、构造器方法、私有方法和父类方法可以唯一确定。这些方法的符号引用在类加载的解析阶段就会解析成直接引用。因此这些方法也被称为非虚方法
特别说明:final 方法由于不能被覆盖,可以唯一确定,因此 Java 语言规范规定 final 方法属于非虚方法,但仍然使用 invokevirtual 指令调用。
(3.4) 运行时多态实现原理-继承
继承:在执行某个方法时,在方法区中找到该类的方法表,再确认该方法在方法表中的偏移量,找到该方法后如果被重写则直接调用,否则认为没有重写父类该方法,这时会按照继承关系搜索父类的方法表中该偏移量对应的方法。
(3.5) 运行时多态实现原理-接口
接口:Java 允许一个类实现多个接口,从某种意义上来说相当于多继承,这样同一个接口的的方法在不同类方法表中的位置就可能不一样了。所以不能通过偏移量的方法,而是通过搜索完整的方法表。
每次接口调用都要搜索方法表,所以从效率上来说,接口方法的调用总是慢于类方法的调用的。
参考资料
[1] Java多态的实现机制是什么,写得非常好!
[2] Java技术——多态的实现原理
[3] Java多态的底层原理
[4] 从jvm虚拟机角度看Java多态 ->(重写override)的实现原理