2014-03-19 131 views
29

這個類在Java 7中編譯好吧,但不是在Java中8:泛型編譯錯誤的Java 8 7

public class Foo { 

    public static void main(String[] args) throws Exception { 
     //compiles fine in Java 7 and Java 8: 
     Class<? extends CharSequence> aClass = true ? String.class : StringBuilder.class; 
     CharSequence foo = foo(aClass); 

     //Inlining the variable, compiles in Java 7, but not in Java 8: 
     CharSequence foo2 = foo(true ? String.class : StringBuilder.class); 

    } 

    static <T> T foo(Class<T> clazz) throws Exception { 
     return clazz.newInstance(); 
    } 
} 

編譯錯誤:

Error:(9, 29) java: method foo in class Foo cannot be applied to given types; required: java.lang.Class found: true ? Str[...]class
reason: inferred type does not conform to equality constraint(s) inferred: java.lang.StringBuilder equality constraints(s): java.lang.StringBuilder,java.lang.String

爲什麼有沒有在Java 8中停止工作?它是否是故意的/某些其他功能的副作用,還是僅僅是一個編譯器錯誤?

+0

這是因爲它改進了類型推斷。 – berry120

+1

@ berry120你認爲這是一個改進? :) –

+0

在這個例子中,我看到了你試圖更清楚地表達的觀點 - 我同意,在我看來,三元操作符並不是以它應該做的方式來推斷類型。 – berry120

回答

10

這是不是一個錯誤的javac,根據當前規範。我在這裏寫了一個答案,是類似於issue的SO。這裏的問題差不多是一樣的。

在賦值或調用上下文中,引用條件表達式是多表達式。這意味着表達式的類型不是將採集轉換應用於lub(T1,T2)的結果,有關T1和T2的詳細定義,請參閱JSL-15.25.3。相反,我們有,還從規範的這一部分是:

Where a poly reference conditional expression appears in a context of a particular kind with target type T, its second and third operand expressions similarly appear in a context of the same kind with target type T.

The type of a poly reference conditional expression is the same as its target type.

因此,這意味着目標類型被向下推到基準條件表達式的兩個操作數,並且兩個操作數都對該目標類型歸屬。因此,編譯器最終收集來自兩個操作數的約束,導致無法解析的約束集,從而導致錯誤。


好的,但爲什麼我們在這裏得到T的平等界限?

讓我們來詳細看看,從呼叫:

foo(true ? String.class : StringBuilder.class) 

其中foo是:

static <T> T foo(Class<T> clazz) throws Exception { 
    return clazz.newInstance(); 
} 

我們有,我們正在調用方法foo()與表達true ? String.class : StringBuilder.class。該引用條件表達式應該在類型爲Class<T>的鬆散調用上下文中兼容。這表現爲,見JLS-18.1.2

true ? String.class : StringBuilder.class → Class<T> 

由於從JLS-18.2.1下面我們有:

A constraint formula of the form ‹Expression → T› is reduced as follows:

...

  • If the expression is a conditional expression of the form e1 ? e2 : e3, the constraint reduces to two constraint formulas, ‹e2 → T› and ‹e3 → T›.

這意味着,我們得到以下約束公式:

String.class → Class<T> 
StringBuilder.class → Class<T> 

或:

Class<String> → Class<T> 
Class<StringBuilder> → Class<T> 

後來從JLS-18.2.2我們有:

A constraint formula of the form ‹S → T› is reduced as follows:

...

  • Otherwise, the constraint reduces to ‹S <: T›.

我只包括相關零部件。所以怎麼回事,我們現在有:

Class<String> <: Class<T> 
Class<StringBuilder> <: Class<T> 

JLS-18.2.3,我們有:

A constraint formula of the form ‹S <: T› is reduced as follows:

...

  • Otherwise, the constraint is reduced according to the form of T:
    • If T is a parameterized class or interface type, or an inner class type of a parameterized class or interface type (directly or indirectly), let A1, ..., An be the type arguments of T. Among the supertypes of S, a corresponding class or interface type is identified, with type arguments B1, ..., Bn. If no such type exists, the constraint reduces to false. Otherwise, the constraint reduces to the following new constraints: for all i (1 ≤ i ≤ n), ‹Bi <= Ai›.

那麼作爲Class<T>Class<String>Class<StringBuilder>是參數化的類,這意味着我們現在的限制減少到:

String <= T 
StringBuilder <= T 

也從JLS-18.2.3,我們有:

A constraint formula of the form ‹S <= T›, where S and T are type arguments (§4.5.1), is reduced as follows:

...

  • If T is a type:
    • If S is a type, the constraint reduces to ‹S = T›.

因此,我們最終與這些約束T:

String = T 
StringBuilder = T 

最後,在JLS-18.2.4我們有:

A constraint formula of the form ‹S = T›, where S and T are types, is reduced as follows:

...

  • Otherwise, if T is an inference variable, α, the constraint reduces to the bound S = α.

並且對於類型變量T沒有溶液與界限T = StringT = StringBuilder 。編譯器沒有類型可以替代T來滿足這兩個限制。爲此,編譯器顯示錯誤消息。


因此,根據目前的規範,javac是可以的,但是規範正確嗎?那麼應該調查7和8之間的兼容性問題。由於這個原因,我已經提交JDK-8044053,所以我們可以跟蹤這個問題。

+0

Th可能值得仔細檢查。你提出的建議是,在Java 7中實現JLS的方式與Java 8相比存在一個錯誤。我沒有時間在版本之間傾注更新日誌,但它可能*看起來像*就像這樣,給出你的解釋。 – Makoto

+0

雖然您已經嚴密地關注了JLS,並且可能已經確定了爲什麼會出現錯誤(我沒有時間仔細檢查),但我不明白爲什麼需要JLS-18.2.4要求。其他地方也出現了類似的問題,請參閱https://bugs.openjdk.java.net/browse/JDK-8043980 - 在這裏,它似乎被認爲是一個編譯器錯誤。這可能是JLS錯了 - 這也不是第一次發生;例如,請參閱http://stackoverflow.com/questions/5385743/java-casting-is-the-compiler-wrong-or-is-the-language-spec-wrong-or-am-i-wron。 – davmac

+0

@Makoto,我沒有說的是,鑑於規範的當前狀態,javac給出了正確的答案。我編輯了我的答案來強調這一點。我還添加了一個對我剛剛提出來調查此問題的規格錯誤的引用,並檢查是否存在規格錯誤。 –

17

我打算出去走一走,說這個錯誤(雖然它可能符合或不符合更新的JLS,我承認我沒有詳細閱讀)是由於類型不一致由JDK 8編譯器處理。

一般來說,三元運算符使用相同類型的推理,就好像對於具有基於相同類型參數的形式參數的雙參數方法一樣。例如:

static <T> T foo(Class<? extends T> clazz, Class<? extends T> clazz2) { return null; } 

public static void main(String[] args) { 
    CharSequence foo2 = foo(String.class, StringBuilder.class); 
} 

在這個例子中,T可以被推斷爲是? extends Object & Serializable & CharSequence捕獲。現在,同樣的,在JDK 7,如果我們回到你原來的例子:

CharSequence foo2 = foo(true ? String.class : StringBuilder.class); 

但這幾乎是完全相同的類型推斷如上,但在這種情況下,考慮三元運算符是一種方法,例如:

static <T> T ternary(boolean cond, T a, T b) { 
    if (cond) return a; 
    else return b; 
} 

因此,在這種情況下,如果你通過String.class和StringBuilder.class作爲參數,推斷類型T的是(粗略地講)Class<? extends Object & Serializable & CharSequence>,這正是我們想要的。

事實上,你可以用這種方法取代使用您的三元運算的原代碼段,即:

public class Foo { 

    public static void main(String[] args) throws Exception { 
     //compiles fine in Java 7 and Java 8: 
     Class<? extends CharSequence> aClass = true ? String.class : StringBuilder.class; 
     CharSequence foo = foo(aClass); 

     //Inlining the variable using 'ternary' method: 
     CharSequence foo2 = foo(ternary(true, String.class, StringBuilder.class)); 

    } 

    static <T> T foo(Class<T> clazz) throws Exception { 
     return clazz.newInstance(); 
    } 

    static <T> T ternary(boolean cond, T a, T b) { 
     if (cond) return a; 
     else return b; 
    } 
} 

...現在它編譯Java 7中和8編輯:實際上它也失敗了Java 8!再次編輯:它現在可以工作,Jdk 8u20)。是什麼賦予了?出於某種原因,現在正在對T(在foo方法中)施加等式約束,而不是下限約束。 JLS for Java 7的相關部分是15.12.2.7;對於Java 8,還有一個關於類型推斷的新章節(chapter 18)。

請注意,在調用'ternary'時顯式鍵入T確實允許使用Java 7和8進行編譯,但這看起來應該不是必須的。 Java 7做的是正確的事情,即使存在適合於T的適當類型,Java 8也會出錯。

+1

我正在寫相同的答案。我有點同意你的意見。 –

+0

+1我也同意。我的答案應該刪除。 –

+1

Ahem,我試着用Netbeans 7.4和RC的'javac',它爲三元運算符'...,...,...'方法產生非常相同的錯誤信息'...?...:...' 。 – Holger

0

問題僅出現在參數和賦值的上下文中。即

CharSequence cs1=(true? String.class: StringBuilder.class).newInstance(); 

的作品。與其他答案聲明不同,使用通用的<T> T ternary(boolean cond, T a, T b)方法不起作用。當調用傳遞到像<T> T foo(Class<T> clazz)這樣的通用方法時,這仍然失敗,以便搜索到<T>的實際類型。然而,它在分配示例中起作用

Class<? extends CharSequence> aClass = true ? String.class : StringBuilder.class; 

作爲結果類型已明確指定。當然,類型爲Class<? extends CharSequence>的類型也可以解決其他用例。

問題是該算法被指定爲先查找第二個和第三個操作數的「最小上界」,然後再應用「捕獲轉換」。對於Class<String>Class<StringBuilder>這兩種類型,第一步已經失敗,因此第二步將考慮上下文甚至沒有嘗試。

這是不是一個令人滿意的情況,然而,在這種特殊情況下有一個優雅的選擇:

public static void main(String... arg) { 
    CharSequence cs=foo(true? String::new: StringBuilder::new); 
} 

static <T> T foo(Supplier<T> s) { return s.get(); } 
+3

「問題在於算法被指定爲首先找到第二個和第三個操作數的」最小上界「,然後應用」捕獲轉換「。對於類型和Class 類,第一步已經失敗了,但是爲什麼它失敗了?最小上限應該是Class <?extends Object&CharSequence&Serializable>。 – davmac

+0

@davmac:I並沒有說這是正確的,我只是將它釘在了這一點上,但像你一樣,我無法在新規範中識別出「身份約束」的相關部分(也許)。更多的時間,但我想告訴我迄今爲止發現的東西,但在我看來,一個如此難以閱讀的規範必須*在執行過程中遇到問題...... – Holger

+0

@Holger,好觀察如果參考條件表達式不出現在一個賦值或調用的上下文中,它被認爲是一個獨立的表達式,因爲它不是一個多表達式,所以javac 8機器不適用於它,它的輸入等同於javac 7獲得的那個。 –