歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> JDK8 中的類型推斷與重載解析

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

日期:2017/3/1 9:13:53   编辑:Linux編程

首先從一個例子開始:

下面這段代碼,在 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 的返回類型 Tsyms.objectType 指的就是 Object 方法。

遍歷方法,選擇 String.valueOf 的過程在 com/sun/tools/javac/comp/Resolve.javafindMethod 中。String.valueOf 的每個方法的參數,都會與 get 的返回類型,即 Object 進行匹配檢查,方法是 com/sun/tools/javac/code/Types.java:isSubtype。例如,檢查 String.valueOf(double) 方法時,會檢查 doubleObject 是否匹配。匹配的邏輯是:

  • 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)。那麼結合 doubleget 的返回類型 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,此時參數 treeget()ptchar[]。生成 get() 字節碼時,其返回類型已為 Object,而非 char[]

最後總結一下文中最開始時提到的兩個問題:

  • get() 返回類型的推斷
  • String.valueOf 這個重載方法的選擇

在 JDK6 中

  • get() 返回類型直接設置為 Object
  • String.valueOf 選擇了 String.valueOf(Object)
  • String.valueOfget() 匹配

在 JDK8 中

  • get() 返���類型先設置為 DeferredAttr.DeferredType
  • 遍歷 String.valueOf 的方法,在滿足條件的 String.valueOf(Object)String.valueOf(char[]) 中選擇更為具體的 String.valueOf(char[])get() 的返回類型也為 char[]
  • get() 的返回類型在 desugar 階段被擦除,設置為 Object,同時強制轉為 char[]

Copyright © Linux教程網 All Rights Reserved