首先從一個例子開始:
下面這段代碼,在 JDK6u30 中可以正常工作,但是在 JDK8u65 中會運行失敗,提示類型轉換錯誤,ClassCastException。
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to [C
我看了一下字節碼,泛型函數推出的返回值都是 Object。然而 JDK8 中調用重載過的函數時,選擇了 String valueOf(char data[])
,本來應該選擇 String.valueOf(Object obj)
,JDK6就是這麼做的,為什麼到 JDK8 反而選擇了一個錯誤的函數呢?
public class TestTypeInference {
public static <T> T get() {
return (T) "x";
}
public static void main(String[] args) {
System.out.println(String.valueOf(get()));
}
// JDK6:
// INVOKESTATIC TestTypeInference.get ()Ljava/lang/Object;
// INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
// JDK8:
// INVOKESTATIC TestTypeInference.get ()Ljava/lang/Object;
// CHECKCAST [C
// INVOKESTATIC java/lang/String.valueOf ([C)Ljava/lang/String;
}
這個問題困擾了我很久,也沒有人可以回答我,只好自己去 javac
的源代碼裡找答案。
首先看 JDK6 中如何通過調試 javac
編譯上面的代碼。從 OpenJDK 上把 JDK6 的 源代碼 下下來,找到 javac 源代碼的入口 openjdk-6-src-b30-21_jan_2014\langtools\src\share\classes\com\sun\tools\javac\main\Main.java
,一層一層調試下去。發現關鍵的地方有兩個:
get()
返回類型的推斷String.valueOf
這個重載方法的選擇在 JDK6 中,直接推斷出 get()
的返回類型是 Object
,然後對 String.valueOf
的所有方法進行遍歷,找到可以將 Object
作為參數的那個方法。最終選擇了 String.valueOf(Object)
。
推斷 get()
返回值類型的入口位於 com.sun.tools.javac.comp.Attr#visitApply
方法中:
argtypes = attribArgs(tree.args, localEnv);
其中 tree.args
此時就是 get()
方法。
而最終將 Object
作為 get()
的返回類型,而位於 com.sun.tools.javac.comp.Check#instantiatePoly
方法,
Type newpt = t.qtype.tag <= VOID ? t.qtype : syms.objectType;
其中 t
指的是 get
的返回類型 T
,syms.objectType
指的就是 Object
方法。
遍歷方法,選擇 String.valueOf
的過程在 com/sun/tools/javac/comp/Resolve.java
的 findMethod
中。String.valueOf
的每個方法的參數,都會與 get
的返回類型,即 Object
進行匹配檢查,方法是 com/sun/tools/javac/code/Types.java:isSubtype
。例如,檢查 String.valueOf(double)
方法時,會檢查 double
與 Object
是否匹配。匹配的邏輯是:
double
是否與 Object
相等Object
是否是 double
子類型最終,會選擇 String.valueOf(Object)
。
而在 JDK8 中,問題變得比較復雜。
JDK8 入口為 com.sun.tools.javac.Main#main
JDK8 中不會推出 get
方法的返回類型為 Object
,而是先設置一個 DeferredAttr.DeferredType
。然後遍歷 String.valueOf
的每個方法。假如當前檢查的是 String.valueOf(double)
。那麼結合 double
和 get
的返回類型 T
,推出 T
的類型為 Double
。這個過程,最終會在 com.sun.tools.javac.comp.Infer#generateReturnConstraintsPrimitive
中體現,返回的是 double
的裝箱類型 Double
。
之後會檢查:
double
是否與 Double
相等。Double
是否是 double
的子類型。這個過程會在 com.sun.tools.javac.comp.Check#checkType(com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition, com.sun.tools.javac.code.Type, com.sun.tools.javac.code.Type, com.sun.tools.javac.comp.Check.CheckContext)
函數中的 checkContext.compatible
進行檢查。
這時候,會發現,只有 String.valueOf(char[])
和 String.valueOf(Object)
滿足了條件。並且 char[]
是 Object
的子類型,即是更具體的類型。所以,最終選擇了 String.valueOf(char[])
。檢查更具體類型的代碼在 com.sun.tools.javac.comp.Resolve#mostSpecific
,最終調用的也是com.sun.tools.javac.comp.Check#checkType(com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition, com.sun.tools.javac.code.Type, com.sun.tools.javac.code.Type, com.sun.tools.javac.comp.Check.CheckContext)
函數方法。檢查 Object
是否是 char[]
的子類型,char[]
是否是 Object
的子類型。以此判斷哪個類型更為具體。
不過在最開始,我們看字節碼時,看出在 JDK8 中,get()
返回的類型應該與 JDK6 中一樣,都是 Object
, 不過在上述的語義分析過程中,看出 get()
返回的是 char[]
。所以接下來,我們看在編譯的其它階段是不是發生了什麼。
在此之前,我們先來看一下編譯過程的入口, com.sun.tools.javac.main.JavaCompiler#compile2
。
下面的代碼是編譯的入口:
generate(desugar(flow(attribute(todo.remove()))));
我們剛才講的都是在 attribute
函數中發生的過程。
接下來,進入 flow
函數,在由 attribute 函數生成的樹上做數據流分析,主要分成四個功能:
然後是 desugar
,看起來是不是挺像解除語法糖的。這個過程會對類進行變換。把 get()
的返回類型由 char[]
變為 Object
的過程,就在這個函數中。最終是由 com.sun.tools.javac.tree.TreeTranslator#translate(T)
這個函數完成。
其中函數 get
會被變換:
public static <T> T get() {
return (T) "x";
}
會變成
public static Object get() {
return (Object) "x";
}
同時,String.valueOf(get())
中的 get()
方法,其類型 char[]
也會變成 Object
。這個過程在 com.sun.tools.javac.comp.TransTypes#retype
中完成。
// tree: get(); erasedType: java.lang.Object; target: char[]
JCExpression retype(JCExpression tree, Type erasedType, Type target) {
..
}
由於 get()
返回類型為 T
,所以這個函數會把 T
換成 Object
,同時插入一條指令,將 Object
轉換成 char[]
。
retype
的本意如下面這個例子所示:
class Cell<A> { A value; }
Cell<Integer> cell;
Integer x = cell.value;
此時,會將 cell.value
返回值設置成 Object
, 並且插入強制類型轉換的指令。
但是在我們這次分析的情況中,get()
的返回類型 T
已經被推斷出是 char[]
,為什麼又因為其定義是 T
,然後被擦除,變為 Object
呢?這樣一來,String.valueOf
選擇了 String.valueOf(char[])
,而 get()
的類型是 Object
,又要強制轉化成 char[]
。既然如此,為什麼不選擇 String.valueOf(Object)
呢?感覺像個 bug 啊。我現在也不能理解為什麼要這麼做。
最後是 generate
,生成字節碼。生成 get()
字節碼的源代碼位於 com.sun.tools.javac.jvm.Gen#genExpr
,此時參數 tree
為 get()
,pt
為 char[]
。生成 get()
字節碼時,其返回類型已為 Object
,而非 char[]
。
最後總結一下文中最開始時提到的兩個問題:
get()
返回類型的推斷String.valueOf
這個重載方法的選擇在 JDK6 中
get()
返回類型直接設置為 Object
String.valueOf
選擇了 String.valueOf(Object)
String.valueOf
與 get()
匹配在 JDK8 中
get()
返���類型先設置為 DeferredAttr.DeferredType
String.valueOf
的方法,在滿足條件的 String.valueOf(Object)
和 String.valueOf(char[])
中選擇更為具體的 String.valueOf(char[])
,get()
的返回類型也為 char[]
get()
的返回類型在 desugar
階段被擦除,設置為 Object
,同時強制轉為 char[]