首先從一個例子開始:
下面這段代碼,在 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 函數生成的樹上做資料流分析,主要分成四個功能:
- AliveAnalyzer: 語句是否可達
- AssignAnalyzer: 變量使用時已經指派;final 變量沒有被多次指派
- FlowAnalyzer: checked 異常是否被聲明及抛出
- CaptureAnalyzer: lambda 表達式或者内部類引用的局部變量要是 final
然後是
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[]