簡體   English   中英

為什么多態不以同樣的方式處理泛型集合和普通數組?

[英]why polymorphism doesn't treat generic collections and plain arrays the same way?

假設類Dog擴展類Animal:為什么不允許這種多態語句:

List<Animal> myList = new ArrayList<Dog>();

但是,它允許使用普通數組:

Animal[] x=new Dog[3];

其原因是基於Java如何實現泛型。

一個數組示例

使用數組你可以做到這一點(數組是協變的,正如其他人所解釋的那樣)

Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;

但是,如果你試圖這樣做會發生什么?

Number[0] = 3.14; //attempt of heap pollution

最后一行編譯得很好,但是如果你運行這段代碼,你可能會得到一個ArrayStoreException 因為您試圖將double放入整數數組(無論是通過數字引用訪問)。

這意味着你可以欺騙編譯器,但你不能欺騙運行時類型系統。 這是因為數組就是我們所說的可再生類型 這意味着在運行時Java知道這個數組實際上被實例化為一個整數數組,它恰好通過Number[]類型的引用進行訪問。

因此,正如您所看到的,有一件事是對象的實際類型,另一件事是您用來訪問它的引用類型,對吧?

Java泛型問題

現在,Java泛型類型的問題是類型信息被編譯器丟棄,並且在運行時不可用。 此過程稱為類型擦除 有很好的理由在Java中實現這樣的泛型,但這是一個很長的故事,它與二進制兼容現有的代碼有關。

但重要的是,由於在運行時沒有類型信息,因此無法確保我們不會造成堆污染。

例如,

List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);

List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap polution

如果Java編譯器沒有阻止你這樣做,那么運行時類型系統也不能阻止你,因為在運行時沒有辦法確定這個列表應該只是一個整數列表。 Java運行時允許你將任何你想要的東西放入這個列表,當它只包含整數時,因為它在創建時被聲明為整數列表。

因此,Java的設計者確保您不能欺騙編譯器。 如果你不能欺騙編譯器(就像我們可以用數組做的那樣),你也不能欺騙運行時類型系統。

因此,我們說泛型類型是不可恢復的

顯然,這會妨礙多態性。 請考慮以下示例:

static long sum(Number[] numbers) {
   long summation = 0;
   for(Number number : numbers) {
      summation += number.longValue();
   }
   return summation;
}

現在你可以像這樣使用它:

Integer[] myInts = {1,2,3,4,5};
Long[] myLongs = {1L, 2L, 3L, 4L, 5L};
Double[] myDoubles = {1.0, 2.0, 3.0, 4.0, 5.0};

System.out.println(sum(myInts));
System.out.println(sum(myLongs));
System.out.println(sum(myDoubles));

但是,如果您嘗試使用泛型集合實現相同的代碼,則不會成功:

static long sum(List<Number> numbers) {
   long summation = 0;
   for(Number number : numbers) {
      summation += number.longValue();
   }
   return summation;
}

如果你試圖...你會得到編譯器錯誤

List<Integer> myInts = asList(1,2,3,4,5);
List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0);

System.out.println(sum(myInts)); //compiler error
System.out.println(sum(myLongs)); //compiler error
System.out.println(sum(myDoubles)); //compiler error

解決方案是學習使用Java泛型的兩個強大功能,稱為協方差和逆變。

協方差

使用協方差,您可以從結構中讀取項目,但不能在其中寫入任何內容。 所有這些都是有效的聲明。

List<? extends Number> myNums = new ArrayList<Integer>();
List<? extends Number> myNums = new ArrayList<Float>()
List<? extends Number> myNums = new ArrayList<Double>()

你可以從myNums

Number n = myNums.get(0); 

因為您可以確定無論實際列表包含什么,它都可以被上傳到一個數字(畢竟任何擴展Number的數字都是數字,對吧?)

但是,不允許將任何內容放入協變結構中。

myNumst.add(45L); //compiler error

這是不允許的,因為Java無法保證通用結構中對象的實際類型。 它可以是擴展Number的任何東西,但編譯器無法確定。 所以你可以閱讀,但不能寫。

逆變

有了逆轉,你可以做相反的事情。 你可以把東西放到一個通用的結構中,但你不能從中讀出來。

List<Object> myObjs = new List<Object();
myObjs.add("Luke");
myObjs.add("Obi-wan");

List<? super Number> myNums = myObjs;
myNums.add(10);
myNums.add(3.14);

在這種情況下,對象的實際性質是對象列表,並且通過逆變,您可以將Numbers放入其中,主要是因為所有數字都將Object作為它們的共同祖先。 因此,所有Numbers都是對象,因此這是有效的。

但是,假設您將獲得一個數字,則無法安全地從這個逆變結構中讀取任何內容。

Number myNum = myNums.get(0); //compiler-error

如您所見,如果編譯器允許您編寫此行,則會在運行時獲得ClassCastException。

獲取/放置原則

因此,當您只打算從結構中獲取通用值時使用協方差,當您只打算將通用值放入結構時使用逆變,並在您打算同時使用完全通用類型時使用。

我所擁有的最好的例子是,將任何類型的數字從一個列表復制到另一個列表中。 它只從源頭獲取物品,它只物品放入命運。

public static void copy(List<? extends Number> source, List<? super Number> destiny) {
    for(Number number : source) {
        destiny.add(number);
    }
}

由於協方差和逆變的力量,這適用於這樣的情況:

List<Integer> myInts = asList(1,2,3,4);
List<Double> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();

copy(myInts, myObjs);
copy(myDoubles, myObjs);

數組在兩個重要方面與通用類型不同。 首先,數組是協變的。 這個可怕的單詞意味着如果Sub是Super的子類型,那么數組類型Sub []是Super []的子類型。 相反,泛型是不變的:對於任何兩個不同類型Type1和Type2,List <Type1>既不是子類型也不是List <Type2>的超類型。

[..]數組和泛型之間的第二個主要區別是數組是有效的[JLS,4.7]。 這意味着數組在運行時知道並強制執行其元素類型。

[..]相反,泛型是通過擦除實現的[JLS,4.6]。 這意味着它們僅在編譯時強制執行其類型約束,並在運行時丟棄(或擦除)其元素類型信息。 擦除允許泛型類型與不使用泛型的遺留代碼自由互操作(第23項)。 由於這些基本差異,陣列和泛型不能很好地混合。 例如,創建泛型類型,參數化類型或類型參數的數組是非法的。 這些數組創建表達式都不合法: new List <E> [],new List <String> [],new E [] 所有這些都會在編譯時導致通用數組創建錯誤。[..]

Prentice Hall - 有效的Java第2版

那非常有趣。 我不能告訴你答案,但是如果你想將Dogs列表放入動物列表中,這是有效的:

List<Animal> myList = new ArrayList<Animal>();
myList.addAll(new ArrayList<Dog>());

編譯集合版本以便編譯的方法是:

List<? extends Animal> myList = new ArrayList<Dog>();

你不需要使用數組的原因是由於類型擦除 - 非基元的數組都只是Object[]而java數組不是類型的類(比如集合)。 該語言從未被設計為迎合它。

數組和泛型不混合。

List<Animal> myList = new ArrayList<Dog>();

是不可能的,因為在那種情況下你可以把貓放入狗:

private void example() {
    List<Animal> dogs = new ArrayList<Dog>();
    addCat(dogs);
    // oops, cat in dogs here
}

private void addCat(List<Animal> animals) {
    animals.add(new Cat());
}

另一方面

List<? extends Animal> myList = new ArrayList<Dog>();

是可能的,但在這種情況下,你不能使用泛型參數的方法(只接受null):

private void addCat(List<? extends Animal> animals) {
    animals.add(null);      // it's ok
    animals.add(new Cat()); // compilation error here
}

最終的答案就是這樣,因為Java是這樣指定的。 更確切地說,因為這是Java規范演變的方式*

我們不能說Java設計者的實際想法是什么,但考慮一下:

List<Animal> myList = new ArrayList<Dog>();
myList.add(new Cat());   // compilation error

Animal[] x = new Dog[3];
x[0] = new Cat();        // runtime error

將在此處拋出的運行時錯誤是ArrayStoreException 這可能會在任何非基元數組的任何賦值上拋出。

人們可以說Java對數組類型的處理是錯誤的...因為上面的例子。

*請注意,在Java 1.0之前指定了Java數組的類型,但是只在Java 1.5中添加了泛型類型。 Java語言具有向后兼容性的總體元要求; 即語言擴展不應該破壞舊代碼。 除此之外,這意味着無法修復歷史錯誤,例如數組鍵入的工作方式。 (假設接受這一個錯誤......)


在泛型類型方面,鍵入erasure des不解釋編譯錯誤。 由於使用非擦除泛型類型進行編譯類型檢查,實際上發生了編譯錯誤。

實際上,您可以通過使用取消選中類型轉換(忽略警告)來破壞編譯錯誤,最終導致ArrayList<Dog>在運行時實際包含Cat對象。 是類型擦除的結果!)但要注意,使用未經檢查的轉換對編譯錯誤的顛覆可能導致意外地點的運行時錯誤......如果你弄錯了。 這就是為什么這是一個壞主意。

在泛型之前的幾天,編寫一個可以對任意類型的數組進行排序的例程將要求能夠(1)以協變方式創建只讀數組,並以獨立於類型的方式交換或重新排列元素,或者(2)創建可以安全讀取的協變方式的讀寫數組,可以使用先前從同一數組中讀取的內容安全地編寫,或者(3)使數組提供一些與類型無關的元素比較方法。 如果從一開始就在語言中包含了協變和逆變通用接口,那么第一種方法可能是最好的,因為它可以避免在運行時執行類型檢查的需要以及這種類型檢查的可能性可能會失敗。 盡管如此,由於不存在這樣的通用支持,因此除了基類型數組之外,沒有任何派生類型數組可以合理地轉換為其他類型。

暫無
暫無

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

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