簡體   English   中英

Java:重載歧義的通用方法

[英]Java: Generic method overloading ambiguity

請考慮以下代碼:

public class Converter {

    public <K> MyContainer<K> pack(K key, String[] values) {
        return new MyContainer<>(key);
    }

    public MyContainer<IntWrapper> pack(int key, String[] values) {
        return new MyContainer<>(new IntWrapper(key));
    }


    public static final class MyContainer<T> {
        public MyContainer(T object) { }
    }

    public static final class IntWrapper {
        public IntWrapper(int i) { }
    }


    public static void main(String[] args) {
        Converter converter = new Converter();
        MyContainer<IntWrapper> test = converter.pack(1, new String[]{"Test", "Test2"});
    }
}

上面的代碼編譯沒有問題。 但是,如果在pack簽名和new String[]{"Test", "Test2"}"Test", "Test2"中將String[]更改為String... ,編譯器會抱怨對converter.pack的調用。曖昧。

現在,我可以理解為什么它可以被認為是模糊的(因為int可以自動裝入Integer ,從而匹配K的條件或缺乏條件)。 但是,我無法理解的是,如果你使用String[]而不是String... ,那么歧義就不存在了。

有人可以解釋這個奇怪的行為嗎?

你的第一個案例非常簡單。 以下方法:

public MyContainer<IntWrapper> pack(int key, Object[] values) 

是參數的完全匹配 - (1, String[]) 來自JLS第15.12.2節

第一階段(§15.12.2.2)執行重載解析而不允許裝箱或拆箱轉換

現在,將這些參數傳遞給第二個方法時沒有涉及裝箱。 As Object[]String[]的超類型。 並且,即使在Java 5之前,為Object[]參數傳遞String[]參數也是一個有效的調用。


編譯器似乎在你的第二種情況下玩耍:

在你的第二種情況下,因為你已經使用了var-args,所以使用var-args,裝箱或拆箱來完成方法重載分辨率,按照JLS部分中解釋的第3階段:

第三階段(§15.12.2.4)允許重載與變量arity方法,裝箱和拆箱相結合。

注意,由於使用var-args ,第二階段不適用於此:

第二階段(§15.12.2.3)執行重載解析,同時允許裝箱和拆箱,但仍然排除使用變量arity方法調用。

現在發生的事情是 編譯器沒有正確地推斷出類型參數* (實際上,它正確地推斷它,因為類型參數被用作形式參數,請參閱本答案末尾的更新)。 所以,對於你的方法調用:

MyContainer<IntWrapper> test = converter.pack(1, "Test", "Test2");

編譯器應該從LHS推斷泛型方法中的K類型為IntWrapper 但似乎它推斷K是一個Integer類型,因為你的方法現在同樣適用於這個方法調用,因為它們都需要var-argsboxing

但是,如果該方法的結果未分配給某個引用,那么我可以理解編譯器無法推斷出正確的類型,因為在這種情況下,給出歧義錯誤是完全可以接受的:

converter.pack(1, "Test", "Test2");

可能是我猜,只是為了保持一致性,第一種情況也明顯不明確。 但是,我並不確定,因為我沒有從JLS或其他官方參考資料中找到任何有關此問題的可信來源。 我將繼續搜索,如果我找到一個,將更新答案。


讓我們通過顯式類型信息來欺騙編譯器:

如果更改方法調用以提供顯式類型信息:

MyContainer<IntWrapper> test = converter.<IntWrapper>pack(1, "Test", "Test2");

現在,類型K將被推斷為IntWrapper ,但由於1不能轉換為IntWrapper ,因此該方法將被丟棄,並且第二個方法將被調用並且它將完美地工作。


坦率地說,我真的不知道這里發生了什么。 我希望編譯器在第一種情況下從方法調用上下文中推斷出類型參數,因為它適用於以下問題:

public static <T> HashSet<T> create(int size) {  
    return new HashSet<T>(size);  
}
// Type inferred as `Integer`, from LHS.
HashSet<Integer> hi = create(10);  

但是,在這種情況下它並沒有這樣做。 所以這可能是一個錯誤。

*或者,當類型未作為參數傳遞時,我可能不完全理解編譯器如何推斷類型參數。 因此,為了更多地了解這一點,我試圖通過--JLS§15.12.2.7JLS§15.12.2.8 ,這是關於編譯器如何推斷類型參數,但這完全超出了我的頭腦。

所以,現在你必須忍受它,並使用替代方法(提供顯式類型參數)。


事實證明,Compiler沒有玩任何技巧:

正如@ zhong.j.yu。在評論中最后解釋的那樣,編譯器僅在第15.12.2.8節中對類型推斷應用,當它無法根據15.12.2.7部分推斷它時。 但是在這里,它可以從傳遞的參數推斷出類型為Integer ,因為類型參數顯然是方法中的格式參數。

所以,是的編譯器正確地將類型推斷為Integer ,因此歧義是有效的。 現在我覺得這個答案已經完成了。

在這里,您可以看出以下兩種方法的區別:方法1:

   public MyContainer<IntWrapper> pack(int key, Object[] values) {
    return new MyContainer<>(new IntWrapper(""));
   }

方法2:

public MyContainer<IntWrapper> pack(int key, Object ... values) {
    return new MyContainer<>(new IntWrapper(""));
}

方法2也一樣好

public MyContainer<IntWrapper> pack(Object ... values) {
    return new MyContainer<>(new IntWrapper(""));
 }

這就是為什么你會有歧義的原因..

編輯是的我想說它們在編譯時是一樣的。 使用變量參數的全部目的是使用戶能夠在他/她不確定給定類型的參數數量時定義方法。

因此,如果您使用對象作為變量參數,您只需說編譯器我不確定將發送多少個對象,另一方面,您說,“我傳遞的是整數和未知數量的對象”。 對於編譯器,整數也是一個對象。

如果要檢查有效性,請嘗試傳遞一個整數作為第一個參數,然后傳遞String的變量參數。 你會看到差異。

例如:

public class Converter {
public static void a(int x, String... y) {
}

public static void a(String... y) {
}

public static void main(String[] args) {
    a(1, "2", "3");
}
}

另外,請不要互換使用數組和變量args,它們有一些不同的用途。

當您使用varargs時,該方法不會期望一個數組,但可以使用相同類型的不同參數,這些參數可以以索引方式訪問。

在這種情況下

(1) m(K,   String[])
(2) m(int, String[])

m(1, new String[]{..});

m(1)滿足15.12.2.3。 階段2:確定方法調用轉換適用的匹配Arity方法

m(2)滿足15.12.2.2。 階段1:確定子類型適用的匹配Arity方法

編譯器在第1階段停止; 它發現m(2)是該階段唯一適用的方法,因此選擇m(2)。

在var arg的情況下

(3) m(K,   String...)
(4) m(int, String...)

m(1, str1, str2);

m(3)和m(4)都滿足15.12.2.4。 階段3:確定適用的變量Arity方法 兩者都沒有比另一個更具體,因此含糊不清。

我們可以將適用的方法分為4組:

  1. 適用於子類型
  2. 適用於方法調用轉換
  3. vararg,適用於子類型
  4. vararg,適用於方法調用轉換

規范合並了第3組和第4組,並在第3階段對它們進行處理。因此不一致。

他們為什么這樣做? Maye他們只是厭倦了它。

另一個批評是,不應該有所有這些階段,因為程序員不會這樣思考。 我們應該簡單地找到所有適用的方法,然后選擇最具體的方法(有一些機制來避免裝箱/拆箱)

首先,這只是一些第一線索......可以編輯更多。

編譯器始終搜索並選擇可用的最具體方法。 雖然閱讀有點笨拙,但它在JLS 15.12.2.5中都有規定。 因此,通過電話

converter.pack(1,“Test”,“Test2”)

對於編譯器來說, 1是否應該解散為Kint是不可確定的。 換句話說,K可以應用於任何類型,因此它與int / Integer處於同一級別。

不同之處在於參數的數量和類型。 考慮new String[]{"Test", "Test2"}是一個數組,而"Test", "Test2"是String類型的兩個參數!

converter.pack(1); //模棱兩可,編譯錯誤

converter.pack(1,null); //調用方法2,編譯器警告

converter.pack(1,new String [] {}); //調用方法2,編譯器警告

converter.pack(1,new Object()); //模糊,編譯錯誤

converter.pack(1,new Object [] {}); //調用方法2,沒有警告

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM