2013-07-03 106 views
20

以下測試程序是從更復雜的程序派生而來的,它可以做一些有用的事情。它使用Eclipse編譯器成功編譯。Java可以從類型參數邊界推斷出類型參數嗎?

import java.util.ArrayList; 
import java.util.List; 

public class InferenceTest 
{ 
    public static void main(String[] args) 
    { 
     final List<Class<? extends Foo<?, ?>>> classes = 
      new ArrayList<Class<? extends Foo<?, ?>>>(); 
     classes.add(Bar.class); 
     System.out.println(makeOne(classes)); 
    } 

    private static Foo<?, ?> makeOne(Iterable<Class<? extends Foo<?, ?>>> classes) 
    { 
     for (final Class<? extends Foo<?, ?>> cls : classes) 
     { 
      final Foo<?, ?> foo = make(cls); // javac error here 
      if (foo != null) 
       return foo; 
     } 
     return null; 
    } 

    // helper used to capture wildcards as type variables 
    private static <A, B, C extends Foo<A, B>> Foo<A, B> make(Class<C> cls) 
    { 
     // assume that a real program actually references A and B 
     try 
     { 
      return cls.getConstructor().newInstance(); 
     } 
     catch (final Exception e) 
     { 
      return null; 
     } 
    } 

    public static interface Foo<A, B> {} 

    public static class Bar implements Foo<Integer, Long> {} 
} 

然而,與Oracle JDK 1.7的javac時,出現這樣的:

InferenceTest.java:18: error: invalid inferred types for A,B; inferred type does not 
conform to declared bound(s) 
      final Foo<?, ?> foo = make(cls); 
            ^
    inferred: CAP#1 
    bound(s): Foo<CAP#2,CAP#3> 
    where A,B,C are type-variables: 
    A extends Object declared in method <A,B,C>make(Class<C>) 
    B extends Object declared in method <A,B,C>make(Class<C>) 
    C extends Foo<A,B> declared in method <A,B,C>make(Class<C>) 
    where CAP#1,CAP#2,CAP#3 are fresh type-variables: 
    CAP#1 extends Foo<?,?> from capture of ? extends Foo<?,?> 
    CAP#2 extends Object from capture of ? 
    CAP#3 extends Object from capture of ? 
1 error 

哪個編譯器是正確的?

上面輸出的一個可疑方面是CAP#1 extends Foo<?,?>。我期望類型變量的範圍是CAP#1 extends Foo<CAP#2,CAP#3>。如果是這種情況,那麼CAP#1的推斷界限將符合所聲明的界限。然而,這可能是一個紅色的鯡魚,因爲C確實應推斷爲CAP#1,但錯誤信息是關於A和B.


需要注意的是,如果我用下面的替代線路26,兩種編譯器接受程序:

private static <C extends Foo<?, ?>> Foo<?, ?> make(Class<C> cls) 

但是,現在我不能參考Foo參數的捕獲類型。

更新:由兩種編譯器(也沒用)類似地接受的是:

private static <A, B, C extends Foo<? extends A, ? extends B>> 
    Foo<? extends A, ? extends B> make(Class<C> cls) 

它實質上導致AB被平凡推斷爲Object,因而顯然不是在任何上下文中是有用的。但是,它確實證實了我的理論,javac只會對通配符邊界執行推理,而不是捕獲邊界。如果沒有人有更好的想法,這可能是(不幸的)答案。 (完更新)


我意識到這整個問題可能TL; DR,但我會​​繼續的情況下,其他人更是創下這個問題...

基於JLS 7,§15.12.2.7 Inferring Type Arguments Based on Actual Arguments ,我已經做了如下分析:

給出的形式,A = F的約束,或A >> F

最初,我們有一個形式爲的約束,表明A類型可以通過方法調用轉換(§5.3)轉換爲F類型。這裏,AClass<CAP#1 extends Foo<CAP#2, CAP#3>>FClass<C extends Foo<A, B>>。請注意,其他約束形式(A = FA >> F)僅在推理算法遞歸時出現。

接着,C應被推斷爲CAP#1由以下規則:

(2。)否則,如果約束的形式爲:

  • 如果F具有形式G<..., Yk-1, U, Yk+1, ...>, 其中U是一種類型的表達,其涉及Tj, 然後如果A具有如下形式的超類型G<..., Xk-1, V, Xk+1, ...> 其中V是一種類型的表達, 該算法遞歸地應用到該約束V = U

這裏,GClassUTjCVCAP#1。遞歸應用到CAP#1 = C應該產生約束C = CAP#1

(3)否則,如果約束的形式A = F

  • 如果F = Tj,那麼約束Tj = A是隱含的。

到目前爲止,分析似乎與javac輸出一致。也許分歧的一點是是否繼續試圖推斷AB。例如,給定此規則

  • 如果F具有形式G<..., Yk-1, ? extends U, Yk+1, ...>, 其中U涉及Tj,然後如果A具有超類型是之一:
    • G<..., Xk-1, V, Xk+1, ...>,其中V是一種類型的表達式。
    • G<..., Xk-1, ? extends V, Xk+1, ...>

然後該算法遞歸地應用到該約束V << U

如果CAP#1被認爲是一個通配符(它的捕獲),那麼這個規則適用,並推斷與U作爲Foo<A, B>VFoo<CAP#2, CAP#3>繼續遞歸。如上所述,這將產生A = CAP#2B = CAP#3

但是,如果CAP#1只是一個類型變量,那麼沒有任何規則似乎考慮它的邊界。規範中該部分末尾的這種讓步指的是這種情況:

類型推斷算法應該被視爲一種啓發式算法,旨在在實踐中表現良好。如果它無法推斷所需的結果,則可以使用顯式類型參數。

顯然,通配符不能用作顯式類型參數。 :-(

+0

您正在使用什麼版本javac'的'? 'javac -version' – Jeffrey

+0

@Jeffrey:比1.7更具體嗎? 'javac 1.7.0_25' –

+0

@BevynQ:是的,那個例子「解決方法」就在我的問題中。然而,這是沒有用的,因爲'make'的要點是捕獲通配符,例如用於在進一步的方法調用中表示相同類型的多個實例或使用這些類型參數創建其他對象實例。這個問題與結果賦予'Foo '無關;該部分適用於任一編譯器。 –

回答

10

的問題是,你從下面的推論約束啓動:

class<#1>, #1 <: Foo<?, ?>

,讓你對C,一個解決方案,即C = #1。

然後,你需要檢查C是否符合申報範圍 - 綁定的C是富,所以你最終這個檢查:

#1 <: Foo<A,B>

可以改寫爲

Bound(#1) <: Foo<A, B>

因此:

Foo<?, ?> <: Foo<A, B>

現在,這裏的編譯器的LHS(這裏的地方產生#2和#3)的捕獲的轉換:

Foo<#2, #3> <: Foo<A, B>

這意味着

A = #2

B = #3

所以,我們的解決方案是{A =#2,B =#3,C =#1}。

這是一個有效的解決方案嗎?爲了回答這個問題,我們需要檢查的推斷類型是否與推理可變範圍兼容,取代後,那麼:

[A:=#2]A <: Object
#2 <: Object - ok

[B:=#3]B <: Object
#3 <: Object - ok

[C:=#1]C <: [A:=#2, B:=#3]Foo<A, B>
#1 <: Foo<#2, #3>
Foo<?, ?> <: Foo<#2, #3>
Foo<#4, #5> <: Foo<#2, #3> - not ok

因此,錯誤。

,當談到推理和捕捉類型之間的相互影響的規範是尚未,所以這是很正常的(但不是好!)不同的編譯器之間切換時有不同的行爲。然而,從編譯器和JLS的角度來看,其中一些問題正在得到解決,所以像這樣的問題應該在中期得到解決。

+0

感謝您的快速響應!爲什麼'Foo '被重新引入新的捕獲?如果編譯器在推理階段的檢查階段執行單獨的捕獲轉換,似乎永遠不可能有通配符可滿足的(非平凡的)推斷約束。 IOW,#2和#4對應相同的通配符,但#4 <:#2>永遠不會成立。 JLS似乎並不需要(事實上,根本沒有提及該部分的捕獲),並且它沒有任何解決方法就打破了合理的代碼,所以這是一個javac錯誤,對吧? –

+0

這是一個規格問題 - 規範首先要求新的捕獲;我的看法是,根據執行後推理檢查的方式,某種編譯器可能會受此行爲的影響。 Javac當然是。 但是你所說的是100%發現:在子類型化過程中產生捕獲的變量作爲推理過程的一部分會導致不可滿足的約束 - 這就是爲什麼該規範正在這個領域進行重構。 –

+0

謝謝,這很好聽。你知道規範工作是否針對Java 8,9或更高版本嗎?就目前而言,我只是使用了原始模型作爲解決方法,因爲我從Eclipse中獲得了信心,即邏輯是正確的。 –

1

兩件事情我已經注意到:

  1. CAP#1不是通配符,那是因爲capture conversion類型變量

  2. 在第一步,JLS提到。 U是類型表達式Tj是類型參數。JLS沒有明確定義什麼類型表達式是的,但我的直覺是它包含了類型參數的邊界。如果是這種情況,U將是C extends Foo<A,B>V將是CAP#1 extends Foo<CAP#2, CAP#3>。繼類型推理算法:

V = U - >C = CAP#1Foo<CAP#2, CAP#3> = Foo<A, B>

可以繼續類型推理算法的應用上面,你將結束與A= CAP#2B=CAP#3

我相信你已經發現了與Oracle的編譯器錯誤

+0

同意,'CAP#n'是一個由於通配符捕獲轉換而引入的類型變量。我在那裏得到的是捕獲轉換是否在鍵入推斷之前完全發生。它花了一段時間才找到它,但答案似乎是肯定的:「如果表達式名稱出現在它受方法調用轉換[[]]影響的上下文中,則表達式的類型name是捕獲轉換後字段,局部變量或參數的聲明類型。[§6.5.6.1](http://docs.oracle.com/javase/specs/jls/se7/html/jls-6.html #JLS-6.5.6。1) –