Java 基础:动态代理、多态原理

Posted by Piasy on January 17, 2017
本文是 Piasy 原创,发表于 https://blog.piasy.com,请阅读原文支持原创 https://blog.piasy.com/2017/01/17/Java-Basics-Dynamic-proxy-and-Polymorphism/

RESTful API 调用很多人都在用 Retrofit,说到 Retrofit 就不得不提动态代理,虽然这不是它唯一的亮点,而且也不是动态代理的典型使用场景,但大家就是爱问:说说动态代理是怎么回事吧?Retrofit 的解析请见 拆轮子系列:拆 Retrofit

动态代理的原理

看过源码之后其实很简单,就是一句话:运行时生成实现类(代理类)的字节码,对其所有的方法调用都转发到 invocation handler 的 invoke 方法,在 invoke 方法中执行额外的逻辑。代理类的生成有个缓存优化(比较复杂,不展开)。

为什么说 Retrofit 对动态代理的使用并非典型场景呢?通常来说代理类(无论是手写的静态代理,还是动态生成的代理)都只是做些额外的工作,比例进行权限检查、相关初始化,实际工作还是由委托类(被代理的类)完成,但 Retrofit 实际上没有委托类,我们只负责定义接口,所有的工作都由动态生成的代理完成。所以说并非动态代理的典型场景。

接下来再问几个问题:

  • 对接口方法的调用是怎么一步步实际执行到 invoke 的?
  • 虚函数表,运行时多态,编译时多态?
  • Java 的虚函数表存在哪里的?

当然这些问题可能很多人不会问,但我还是想聊一聊。

多态的原理

多态通常可以分为两种:编译时多态和运行时多态。其实在 Java 里面,还有一个类似的分类:重载(overload)和重写(override)。可以认为编译时多态等于重载,运行时多态等于重写。

编译时多态指的就是函数名相同,但是参数列表不同。例如 int add(int a, int b)float add(float a, float b),在代码编译的时候就可以知道调用的是哪个版本,怎么确定的?参数列表呀!因为函数版本在编译时确定,所以称为编译时多态。

运行时多态则是指虚函数的多态,父类和子类、不同子类的实现不一样。这里就不举例了,Java 里面绝大多数方法都是虚函数,除了构造函数、final、private、static 函数(final 比较特殊)。而在 C++ 中,只有用 virtual 关键字修饰的方法才是虚函数。由于我们可以把子类对象赋值给父类引用,所以到底应该执行父类的版本还是子类的版本,编译期无法确定,必须在运行时查看这个对象的实际类型,然后再调用这个类型的版本。这种运行时的查找机制,就叫运行时多态。

知道一个对象的具体类型之后,怎么确定应该执行哪个函数版本呢?我们会有一个查找表,里面记录着每个函数名实际函数体的地址,知道一个对象的具体类型之后,就拿到了它的这个查找表,所以就知道应该执行哪个函数版本了。这个查找表就叫虚函数表。

那 Java 的虚函数表存在哪里呢?

这是 JVM 实现细节。但我们按逻辑推断,可以存在 class 对象那里。一个对象包含三部分信息,虚函数表、类型信息、对象特定信息,前两者同一个类的所有对象都共享,所以可以放在 class 对象那里。

参考文章: