JDK8在泛型類型推導上的變化

概述
最近公司在大麵積推廣JDK8,整體來說升級上來算順利的,大部分問題可能在編譯期就碰到了,但是有些時候比較蛋疼,編譯期沒有出現問題,但是在運行期就出了問題,比如今天要說的這個話題,所以大家再升級的時候還是要多測測再上線,當然JDK8給我們帶來了不少紅利,花點時間升級上來還是值得的。
問題描述
還是老規矩,先上demo,讓大家直觀地知道我們要說的問題。
public class Test {
static <T extends Number> T getObject() {
return (T)Long.valueOf(1L);
}
public static void main(String... args) throws Exception {
StringBuilder sb = new StringBuilder();
sb.append(getObject());
}
}
demo很簡單,就是有個使用了泛型的函數getObject,其返回類型是Number的子類,然後我們將函數返回值傳給StringBuilder的多態方法append,我們知道append方法有很多,參數類型也很多,但是唯獨沒有參數是Number的append方法,如果有的話,大家應該猜到會優先選擇這個方法了,既然沒有,那到底會選哪個呢,我們分別用jdk6(jdk7類似)和jdk8來編譯下上麵的類,然後用javap看看輸出結果(隻看main方法):
jdk6編譯的字節碼:
public static void main(java.lang.String...) throws java.lang.Exception;
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=2, locals=2, args_size=1
0: new #3 // class java/lang/StringBuilder
3: dup
4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
7: astore_1
8: aload_1
9: invokestatic #5 // Method getObject:()Ljava/lang/Number;
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
15: pop
16: return
LineNumberTable:
line 8: 0
line 9: 8
line 10: 16
Exceptions:
throws java.lang.Exception
jdk8編譯的字節碼:
public static void main(java.lang.String...) throws java.lang.Exception;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=2, locals=2, args_size=1
0: new #3 // class java/lang/StringBuilder
3: dup
4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
7: astore_1
8: aload_1
9: invokestatic #5 // Method getObject:()Ljava/lang/Number;
12: checkcast #6 // class java/lang/CharSequence
15: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/CharSequence;)Ljava/lang/StringBuilder;
18: pop
19: return
LineNumberTable:
line 8: 0
line 9: 8
line 10: 19
Exceptions:
throws java.lang.Exception
對比上麵那個的差異,我們看到bci從12開始變了,jdk8裏多了下麵這行表示要對棧頂的數據做一次類型檢查看是不是CharSequence類型:
12: checkcast #6 // class java/lang/CharSequence
另外調用的StringBuilder的append方法也是不一樣的,jdk7裏是調用的參數為Object類型的append方法,而jdk8裏調用的是CharSequence類型的append方法。
最主要的是在jdk6和jdk8下運行上麵的代碼,在jdk6下是正常跑過的,但是在jdk8下是直接拋出異常的:
Exception in thread "main" java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.CharSequence
at Test.main(Test.java:9)
至此問題整個應該描述清楚了。
問題分析
先來說說如果要我們來做這個java編譯器實現這個功能,我們要怎麼來做,其他的都是很明確的,重點在於如下這段如何來確定append的方法使用哪個:
sb.append(getObject());
我們知道getObject()返回的是個泛型對象,這個對象是Number的子類,因此我們首先會去遍曆StringBuilder的所有可見的方法,包括從父類繼承過來的,找是不是存在一個方法叫做append,並且參數類型是Number的方法,如果有,那就直接使用這個方法,如果沒有,那我們得想辦法找到一個最合適的方法,關鍵就在於這個合適怎麼定義,比如說我們看到有個append的方法,其參數是Object類型的,Number是Object的子類,所以我們選擇這個方法肯定沒問題,假如說另外有個append方法,其參數是Serializable類型(當然其實並沒有這種參數的方法),Number實現了這個接口,我們選擇這個方法也是沒問題的,那到底是Object參數的更合適還是Serializable的更合適呢,還有更甚者,我們知道StringBuilder有個方法,其參數是CharSequence,加入我們傳進去的參數其實既是Number的子類,同時又實現了CharSequence這個接口,那我們究竟要不要選它呢?這些問題我們都需要去考慮,而且各有各的理由,說起來都感覺聽合理的。
JDK6裏泛型的類型推導
這裏分析的是jdk6的javac代碼,不過大致看了下jdk7的這塊針對這個問題的邏輯也差不多,所以就以這塊為例了,jdk6裏的泛型類型推導其實比較簡單,從上麵的輸出結果我們也猜到了,其實就是選了參數為Object類型的append方法,它覺得它是最合適的:
private Symbol findMethod(Env<AttrContext> env,
Type site,
Name name,
List<Type> argtypes,
List<Type> typeargtypes,
Type intype,
boolean abstractok,
Symbol bestSoFar,
boolean allowBoxing,
boolean useVarargs,
boolean operator) {
for (Type ct = intype; ct.tag == CLASS; ct = types.supertype(ct)) {
ClassSymbol c = (ClassSymbol)ct.tsym;
if ((c.flags() & (ABSTRACT | INTERFACE | ENUM)) == 0)
abstractok = false;
for (Scope.Entry e = c.members().lookup(name);
e.scope != null;
e = e.next()) {
//- System.out.println(" e " + e.sym);
if (e.sym.kind == MTH &&
(e.sym.flags_field & SYNTHETIC) == 0) {
bestSoFar = selectBest(env, site, argtypes, typeargtypes,
e.sym, bestSoFar,
allowBoxing,
useVarargs,
operator);
}
}
//- System.out.println(" - " + bestSoFar);
if (abstractok) {
Symbol concrete = methodNotFound;
if ((bestSoFar.flags() & ABSTRACT) == 0)
concrete = bestSoFar;
for (List<Type> l = types.interfaces(c.type);
l.nonEmpty();
l = l.tail) {
bestSoFar = findMethod(env, site, name, argtypes,
typeargtypes,
l.head, abstractok, bestSoFar,
allowBoxing, useVarargs, operator);
}
if (concrete != bestSoFar &&
concrete.kind < ERR && bestSoFar.kind < ERR &&
types.isSubSignature(concrete.type, bestSoFar.type))
bestSoFar = concrete;
}
}
return bestSoFar;
}
上麵的邏輯大概是遍曆當前類(比如這個例子中的StringBuilder)及其父類,依次從他們的方法裏找出一個最合適的方法返回,重點就落在了selectBest這個方法上:
Symbol selectBest(Env<AttrContext> env,
Type site,
List<Type> argtypes,
List<Type> typeargtypes,
Symbol sym,
Symbol bestSoFar,
boolean allowBoxing,
boolean useVarargs,
boolean operator) {
if (sym.kind == ERR) return bestSoFar;
if (!sym.isInheritedIn(site.tsym, types)) return bestSoFar;
assert sym.kind < AMBIGUOUS;
try {
if (rawInstantiate(env, site, sym, argtypes, typeargtypes,
allowBoxing, useVarargs, Warner.noWarnings) == null) {
// inapplicable
switch (bestSoFar.kind) {
case ABSENT_MTH: return wrongMethod.setWrongSym(sym);
case WRONG_MTH: return wrongMethods;
default: return bestSoFar;
}
}
} catch (Infer.NoInstanceException ex) {
switch (bestSoFar.kind) {
case ABSENT_MTH:
return wrongMethod.setWrongSym(sym, ex.getDiagnostic());
case WRONG_MTH:
return wrongMethods;
default:
return bestSoFar;
}
}
if (!isAccessible(env, site, sym)) {
return (bestSoFar.kind == ABSENT_MTH)
? new AccessError(env, site, sym)
: bestSoFar;
}
return (bestSoFar.kind > AMBIGUOUS)
? sym
: mostSpecific(sym, bestSoFar, env, site,
allowBoxing && operator, useVarargs);
}
這個方法的主要邏輯落在rawInstantiate這個方法裏(具體代碼不貼了,有興趣的去看下代碼,我將最終最關鍵的調用方法argumentsAcceptable貼出來,主要做參數的匹配),如果當前方法也合適,那就和之前挑出來的最好的方法做一個比較看誰最適合,這個選擇過程在最後的mostSpecific方法裏,其實就和冒泡排序差不多,不過是找最接近的那個類型(逐層找對應是父類的方法,和最小公倍數有點類似)。
boolean argumentsAcceptable(List<Type> argtypes,
List<Type> formals,
boolean allowBoxing,
boolean useVarargs,
Warner warn) {
Type varargsFormal = useVarargs ? formals.last() : null;
while (argtypes.nonEmpty() && formals.head != varargsFormal) {
boolean works = allowBoxing
? types.isConvertible(argtypes.head, formals.head, warn)
: types.isSubtypeUnchecked(argtypes.head, formals.head, warn);
if (!works) return false;
argtypes = argtypes.tail;
formals = formals.tail;
}
if (formals.head != varargsFormal) return false; // not enough args
if (!useVarargs)
return argtypes.isEmpty();
Type elt = types.elemtype(varargsFormal);
while (argtypes.nonEmpty()) {
if (!types.isConvertible(argtypes.head, elt, warn))
return false;
argtypes = argtypes.tail;
}
return true;
}
針對具體的例子其實就是看StringBuilder裏的哪個方法的參數是Number的父類,如果不是就表示沒有找到,如果參數都符合期望就表示找到,然後返回。
所以jdk6裏的這塊的邏輯相對來說比較簡單。
JDK8裏泛型的類型推導
jdk8裏的推導相對來說比較複雜,不過大部分邏輯和上麵的都差不多,但是argumentsAcceptable這塊的變動比較大,增加了一些數據結構,規則變得更加複雜,考慮的場景也更多了,因為代碼嵌套層數很深,具體的代碼我就不貼了,有興趣的自己去跟下代碼(具體變化可以從AbstractMethodCheck.argumentsAcceptable這個方法開始)。
針對具體這個demo,如果getObject返回的對象既實現了CharSequence,又是Number的子類,那它認為這種情況其實選擇參數為CharSequence類型的append方法比參數為Object類型的方法更合適,看起來是要求更嚴格一些了,適用範圍收窄了一些,不是去匹配大而範的接口方法,因此其多加了一層checkcast的檢查,不過我個人觀點是覺得這塊有點太激進了。
最後更新:2017-04-11 19:32:01