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

本文來自: PerfMa技術社區

PerfMa(笨馬網絡)官網java

概述

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方法):app

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方法。ui

最主要的是在jdk6和jdk8下運行上面的代碼,在jdk6下是正常跑過的,可是在jdk8下是直接拋出異常的:code

Exception in thread "main" java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.CharSequence
    at Test.main(Test.java:9)

至此問題整個應該描述清楚了。orm

問題分析

先來講說若是要咱們來作這個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的檢查,不過我我的觀點是以爲這塊有點太激進了。

一塊兒來學習吧

PerfMa KO 系列課之 JVM 參數【Memory篇】

一次 Docker 容器內大量殭屍進程排查分析

相關文章
相關標籤/搜索