天天看點

JDK8 中的類型推斷與重載解析

首先從一個例子開始:

下面這段代碼,在 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[]