《深入理解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);
}
}
當然我們也可以用CGLIB等操縱字節碼的方式來實現動態代理以擺脫對接口的依賴,這裏就不多講了。本系列的重點是介紹java7引入的invokedynamic虛擬機指令及其輔助類和接口,這是第一篇,相當於一個引子。
另外,invokedynamic我們日常其實是用不到的,除非在JVM上製作其它動態語言;Java的Lambda表達式也是用它實現的。