《深入理解Java虚拟机》一书曾经提到过方法分派问题。即一种多态语言是如何决定调用哪个同名函数的。
Java函数的选择分为静态选择(编译期,正式叫法是method overload resolution)和动态分派(运行期)两步,静态分派是根据接收者的声明类型(或曰静态类型)和参数个数以及参数的声明类型决定的;动态分派是根据接收者的实际类型决定的。两者分别对应着重载和重写。也就是说,一次虚函数调用使用哪个重载函数是编译期决定的,使用哪个重写函数则是运行期决定的。
在编译期生成函数调用字节码(如某个invokevirtual或invokeinterface指令)时,会根据接收者的静态类型、函数名、参数个数、参数静态类型选出适用的(applicable)、可访问的(accessble),再选择一个最贴近的(specific),将其符号引用作为本次invokevirtual的参数。
到了运行期,会首先取接收者的实际类型(通过oop-klass对象模型的运行期类型),再从这个类型里找出与编译期选出的那个函数相同特征(signature)的函数,作为实际执行的函数。
因此,Java的多态是有局限的。即,在编译期,它虽然既看接收者又看参数,但它无法推断出每次调用的实际类型——不管是接收者的实际类型还是参数的实际类型,而只能根据它们的静态类型选择函数;在运行期,它又只关心接收者的实际类型而不关心参数的实际类型,也就是只根据接收者的实际类型去进行动态分派(这叫做单分派 single dispatch,c#已经实现了多分派)。
那么是否可以在Java上实现多分派呢?当然可以。最简单的想法莫过于用动态代理的办法拦截所有想要进行多分派的函数,然后加入多分派逻辑。
本系列教程将使用下列接口和类来进行演示。首先,定义一个接口,Friendly:
package multidispatch;
public interface Friendly {
public void sayHelloTo(Friendly another);
public String getName();
}
然后有一个类实现该接口:
package multidispatch;
public class Human implements Friendly {
protected String name;
public Human(String name) {
this.name = name;
}
public Human() {
name = "Unnamed";
}
@Override
public String getName() {
return name;
}
@Override
public void sayHelloTo(Friendly another) {
System.out.println("Hello " + another.getName() + ", I'm " + name);
}
}
再然后,定义Human的两个子类,Man 和 Woman:
package multidispatch;
public class Man extends Human {
public Man(String name) {
super(name);
}
public Man() {
super();
}
public void sayHelloTo(Man another) {
System.out.println("Yoo " + another.name + ", I'm " + name);
}
public void sayHelloTo(Woman another) {
System.out.println("Hi " + another.name + ", I'm " + name);
}
}
package multidispatch;
public class Woman extends Human {
public Woman(String name) {
super(name);
}
public Woman() {
super();
}
public void sayHelloTo(Man another) {
System.out.println("Hi.");
}
public void sayHelloTo(Woman another) {
System.out.println("Hey " + another.name + ", I'm " + name);
}
}
以上代码完成的唯一功能就是相互打招呼。通用的sayHello方法定义在父类Human里,会说“Hello 某某某, I'm 某某某”。而具体的打招呼方式则男女大不同。一个男生跟另一个男生会说Yoo!,一个女生跟另一个女生会亲热的说Hey!男的对女的会如常介绍自己,但他说Hi不说Hello,而女生面对男生则比较矜持,只简单地说一个字:Hi。
然鹅,如果我们按照Java面向接口的编程方式,写如下代码:
package multidispatch;
public class Main {
public static void main(String[] args) {
Friendly tom = new Man("Tom");
Friendly jerry = new Man("Jerry");
Friendly jessie = new Woman("Jessie");
Friendly mary = new Woman("Mary");
// This is single-dispatch
tom.sayHelloTo(jerry);
jerry.sayHelloTo(mary);
jessie.sayHelloTo(mary);
mary.sayHelloTo(tom);
}
}
就会发现,所有的sayHello调用都调用了父类Human里面的那个函数。都打出了“Hello 某某某, I'm 某某某”。这是因为Java不会根据参数的实际类型来进行分派。
下面我们用最简单的JDK动态代理来添加根据实际参数类型进行分派的功能。首先写一个InvocationHandler:
package multidispatch.dynproxyimpl;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import multidispatch.Friendly;
public class SayHelloRedispatcher implements InvocationHandler {
private Object subject;
public SayHelloRedispatcher(Object subject) {
if(!(subject instanceof Friendly)) {
throw new IllegalArgumentException("Must be an instance of Friendly!");
}
this.subject = subject;
}
Friendly getSubject() {
return (Friendly)subject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
String methodName = method.getName();
if("sayHelloTo".equals(methodName) && args != null && args.length == 1) {
try {
// Re-dispatch according to the actual type of argument
Class<?> argCl = args[0].getClass();
// MethodHandle implementation:
MethodType mt = MethodType.methodType(void.class, argCl);
MethodHandle redispatched = MethodHandles.lookup()
.findVirtual(subject.getClass(), methodName, mt)
.bindTo(subject);
return redispatched.invoke(args[0]);
// Reflection implementation:
//Method redispatched = subject.getClass().getMethod(m, argCl);
//return redispatched.invoke(subject, args);
} catch(Throwable t) {
// In any other case, we invoke the original method on subject
t.printStackTrace();
System.out.println(t + " - No re-dispatching..");
}
}
return method.invoke(subject, args);
}
}
可以看到,在invoke函数里,我们首先取得第一个参数的实际类型,然后用JDK7提供的MethodHandle机制找出receiver(subject)对象里符合该实参类型(这里表现为一个MethodType对象)的sayHello函数,直接调用这个MethodHandle即可。当然也可以用反射实现,见注释掉的部分。
然后我们的main也要修改为使用Proxy对象,而不是原始对象:
package multidispatch.dynproxyimpl;
import java.lang.reflect.Proxy;
import multidispatch.Friendly;
import multidispatch.Man;
import multidispatch.Woman;
public class Main {
static Friendly getProxy(Friendly f) {
SayHelloRedispatcher handler = new SayHelloRedispatcher(f);
Class<?>[] interfs = new Class<?>[] {Friendly.class};
Friendly proxy = (Friendly)Proxy.newProxyInstance(
Friendly.class.getClassLoader(), interfs, handler);
return proxy;
}
public static void main(String args[]) {
Friendly tom = new Man("Tom");
Friendly jerry = new Man("Jerry");
Friendly jessie = new Woman("Jessie");
Friendly mary = new Woman("Mary");
Friendly _tom = getProxy(tom);
Friendly _jerry = getProxy(jerry);
Friendly _jessie = getProxy(jessie);
Friendly _mary = getProxy(mary);
// Proxy objects have multi-dispatch ability
_tom.sayHelloTo(jerry);
_jerry.sayHelloTo(mary);
_jessie.sayHelloTo(mary);
_mary.sayHelloTo(tom);
}
}
运行这个Main,就打出多样性的sayHelloTo了。当然我们也可以用CGLIB等操纵字节码的方式来实现动态代理以摆脱对接口的依赖,这里就不多讲了。本系列的重点是介绍java7引入的invokedynamic虚拟机指令及其辅助类和接口,这是第一篇,相当于一个引子。
另外,invokedynamic我们日常其实是用不到的,除非在JVM上制作其它动态语言;Java的Lambda表达式也是用它实现的。