![](/img/trans.png)
[英]Dog is Animal but list<Dog> is not list<Animal>. How to use it safely in a generic/polymorphic function?
[英]Is List<Dog> a subclass of List<Animal>? Why are Java generics not implicitly polymorphic?
我對 Java generics 如何處理 inheritance / 多態性有點困惑。
假設以下層次結構 -
動物(父母)
狗-貓(兒童)
所以假設我有一個方法doSomething(List<Animal> animals)
。 根據 inheritance 和多態性的所有規則,我假設List<Dog>
是List<Animal>
並且List<Cat>
是List<Animal>
- 因此任何一個都可以傳遞給此方法。 不是這樣。 如果我想實現這種行為,我必須通過說doSomething(List<? extends Animal> animals)
明確告訴該方法接受 Animal 的任何子類的列表。
我明白這是 Java 的行為。 我的問題是為什么? 為什么多態一般是隱式的,但是到了generics就必須指定?
不, List<Dog>
不是List<Animal>
。 考慮一下您可以用List<Animal>
做什么——您可以向其中添加任何動物……包括貓。 現在,你能合乎邏輯地將一只貓添加到一窩小狗中嗎? 絕對不。
// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements List
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?
突然你有一只非常困惑的貓。
現在,您不能將Cat
添加到List<? extends Animal>
List<? extends Animal>
因為你不知道它是一個List<Cat>
。 您可以檢索一個值並知道它將是一個Animal
,但您不能添加任意動物。 List<? super Animal>
List<? super Animal>
- 在這種情況下,您可以安全地向其添加一個Animal
,但您不知道可以從中檢索到什么,因為它可能是一個List<Object>
。
您正在尋找的是covariant type parameters 。 這意味着如果 object 的一種類型可以在方法中替換為另一種類型(例如, Animal
可以替換為Dog
),這同樣適用於使用這些對象的表達式(因此List<Animal>
可以替換為List<Dog>
). 問題是協變對於一般的可變列表來說是不安全的。 假設你有一個List<Dog>
,它被用作List<Animal>
。 當您嘗試將 Cat 添加到這個實際上是List<Dog>
的List<Animal>
時會發生什么? 自動允許類型參數協變會破壞類型系統。
添加語法以允許將類型參數指定為協變會很有用,這樣可以避免? extends Foo
在方法聲明中? extends Foo
,但這確實增加了額外的復雜性。
List<Dog>
不是List<Animal>
的原因是,例如,您可以將Cat
插入List<Animal>
,但不能插入List<Dog>
...您可以使用通配符來制作generics 在可能的情況下更具可擴展性; 例如,從List<Dog>
讀取類似於從List<Animal>
讀取——但不是寫入。
Java 語言中的 Generics和Java 教程中關於 Generics的部分對 generics 為什么有些東西是或不是多態或允許的有很好、深入的解釋。
List<Dog>
不是Java 中的List<Animal>
這也是事實
A list of dogs is-英文動物名錄(合理解釋下)
OP 直覺的工作方式——當然是完全有效的——是后一句話。 然而,如果我們應用這種直覺,我們會得到一種在其類型系統中不是 Java 風格的語言:假設我們的語言確實允許將一只貓添加到我們的狗列表中。 那是什么意思? 這意味着該列表不再是狗的列表,而仍然只是動物的列表。 以及哺乳動物列表和四足動物列表。
換句話說:Java中的A List<Dog>
在英文中並不是“a list of dogs”的意思,它的意思是“a list of dogs and nothing other than dogs”。
更一般地說, OP 的直覺適用於一種語言,在這種語言中,對對象的操作可以更改其類型,或者更確切地說,對象的類型是其值的(動態)function。
我會說 Generics 的全部意義在於它不允許這樣做。 考慮 arrays 的情況,它確實允許這種類型的協方差:
Object[] objects = new String[10];
objects[0] = Boolean.FALSE;
該代碼編譯正常,但會引發運行時錯誤(第二行中的java.lang.ArrayStoreException: java.lang.Boolean
)。 它不是類型安全的。 Generics 的要點是添加編譯時類型安全,否則你可以堅持使用沒有 generics 的普通 class。
現在有些時候您需要更加靈活,這就是? super Class
? super Class
和? extends Class
? extends Class
是為了。 前者是當您需要插入類型Collection
(例如)時,后者是當您需要以類型安全的方式從中讀取時。 但同時進行這兩項操作的唯一方法是擁有一個特定的類型。
為了理解這個問題,與 arrays 進行比較是很有用的。
List<Dog>
不是List<Animal>
的子類。
但是Dog[]
是Animal[]
的子類。
Arrays是具體化和協變的。
Reifiable意味着它們的類型信息在運行時是完全可用的。
因此 arrays 提供運行時類型安全但不提供編譯時類型安全。
// All compiles but throws ArrayStoreException at runtime at last line
Dog[] dogs = new Dog[10];
Animal[] animals = dogs; // compiles
animals[0] = new Cat(); // throws ArrayStoreException at runtime
generics 反之亦然:
Generics 被擦除且不變。
因此 generics 不能提供運行時類型安全,但它們提供編譯時類型安全。
在下面的代碼中,如果 generics 是協變的,則可能會在第 3 行造成堆污染。
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
animals.add(new Cat());
這里給出的答案並沒有完全說服我。 因此,我舉了另一個例子。
public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
consumer.accept(supplier.get());
}
聽起來不錯,不是嗎? 但是您只能為Animal
傳遞Consumer
和Supplier
。 如果您有一個Mammal
消費者,但是一個Duck
供應商,盡管它們都是動物,但它們不應該適合。 為了禁止這種情況,添加了額外的限制。
而不是上面的,我們必須定義我們使用的類型之間的關系。
例如,
public <A extends Animal> void passOn(Consumer<A> consumer, Supplier<? extends A> supplier) {
consumer.accept(supplier.get());
}
確保我們只能使用為消費者提供正確類型 object 的供應商。
OTOH,我們也可以這樣做
public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<A> supplier) {
consumer.accept(supplier.get());
}
我們 go 另一種方式:我們定義Supplier
的類型並限制它可以放入Consumer
。
我們甚至可以做到
public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<? extends A> supplier) {
consumer.accept(supplier.get());
}
其中,具有Life
-> Animal
-> Mammal
-> Dog
, Cat
等直觀關系,我們甚至可以將Mammal
放入Life
消費者中,但不能將String
放入Life
消費者中。
這種行為的基本邏輯是Generics
遵循類型擦除機制。 因此,在運行時,您無法識別不像arrays
那樣沒有此類擦除過程的collection
類型。 所以回到你的問題......
所以假設有一個方法如下:
add(List<Animal>){
//You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism
}
現在,如果 java 允許調用者將 Animal 類型的列表添加到此方法中,那么您可能會將錯誤的東西添加到集合中,並且在運行時它也會由於類型擦除而運行。 而在 arrays 的情況下,您將獲得此類場景的運行時異常......
因此,從本質上講,這種行為的實現是為了防止將錯誤的東西添加到集合中。 現在我相信存在類型擦除,以便在沒有 generics 的情況下與遺留 java 兼容....
其他人已經很好地解釋了為什么不能將后代列表轉換為超類列表。
但是,許多人訪問此問題以尋找解決方案。
所以,從Java版本10開始解決這個問題如下:
(注意:S = 超類)
List<S> supers = List.copyOf( descendants );
如果絕對安全,這個 function 將執行強制轉換,如果強制轉換不安全,則執行復制。
如需深入解釋(考慮到此處其他答案提到的潛在陷阱),請參閱相關問題和我 2022 年的回答: https://stackoverflow.com/a/72195980/773113
子類型對於參數化類型是不變的。 即使 class Dog
是Animal
的子類型,參數化類型List<Dog>
也不是List<Animal>
的子類型。 相反,arrays 使用協變子類型,因此數組類型Dog[]
是Animal[]
的子類型。
不變子類型確保不違反 Java 強制執行的類型約束。 考慮@Jon Skeet 給出的以下代碼:
List<Dog> dogs = new ArrayList<Dog>(1);
List<Animal> animals = dogs;
animals.add(new Cat()); // compile-time error
Dog dog = dogs.get(0);
正如@Jon Skeet 所述,此代碼是非法的,因為否則它會在狗預期時返回貓,從而違反類型約束。
將上面的代碼與 arrays 的類似代碼進行比較是有益的。
Dog[] dogs = new Dog[1];
Object[] animals = dogs;
animals[0] = new Cat(); // run-time error
Dog dog = dogs[0];
該代碼是合法的。 但是,拋出數組存儲異常。 數組在運行時以這種方式攜帶其類型 JVM 可以強制協變子類型化的類型安全。
為了進一步理解這一點,讓我們看看下面 class 的javap
生成的字節碼:
import java.util.ArrayList;
import java.util.List;
public class Demonstration {
public void normal() {
List normal = new ArrayList(1);
normal.add("lorem ipsum");
}
public void parameterized() {
List<String> parameterized = new ArrayList<>(1);
parameterized.add("lorem ipsum");
}
}
使用命令javap -c Demonstration
,這顯示了以下 Java 字節碼:
Compiled from "Demonstration.java"
public class Demonstration {
public Demonstration();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void normal();
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: iconst_1
5: invokespecial #3 // Method java/util/ArrayList."<init>":(I)V
8: astore_1
9: aload_1
10: ldc #4 // String lorem ipsum
12: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
17: pop
18: return
public void parameterized();
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: iconst_1
5: invokespecial #3 // Method java/util/ArrayList."<init>":(I)V
8: astore_1
9: aload_1
10: ldc #4 // String lorem ipsum
12: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
17: pop
18: return
}
觀察方法體的翻譯代碼是相同的。 編譯器用擦除替換了每個參數化類型。 此屬性至關重要,意味着它不會破壞向后兼容性。
總之,運行時安全對於參數化類型是不可能的,因為編譯器通過擦除來替換每個參數化類型。 這使得參數化類型只不過是語法糖。
實際上你可以使用一個接口來實現你想要的。
public interface Animal {
String getName();
String getVoice();
}
public class Dog implements Animal{
@Override
String getName(){return "Dog";}
@Override
String getVoice(){return "woof!";}
}
然后您可以使用集合使用
List <Animal> animalGroup = new ArrayList<Animal>();
animalGroup.add(new Dog());
如果您確定列表項是給定超類型的子類,則可以使用以下方法轉換列表:
(List<Animal>) (List<?>) dogs
當你想在構造函數中傳遞列表或迭代它時,這很有用。
答案以及其他答案都是正確的。 我將使用我認為有用的解決方案添加到這些答案中。 我認為這在編程中經常出現。 需要注意的一件事是,對於 Collections(列表、集合等),主要問題是添加到集合中。 這就是事情崩潰的地方。 即使刪除也可以。
在大多數情況下,我們可以使用Collection<? extends T>
Collection<? extends T>
而不是Collection<T>
,這應該是首選。 但是,我發現在某些情況下不容易做到這一點。 關於這是否總是最好的做法還有待討論。 我在這里展示一個 class DownCastCollection,它可以轉換一個Collection<? extends T>
Collection<? extends T>
到Collection<T>
(我們可以為 List、Set、NavigableSet 等定義類似的類),在使用標准方法時非常不方便。 下面是一個如何使用它的示例(在這種情況下我們也可以使用Collection<? extends Object>
,但我盡量簡單地說明如何使用 DownCastCollection。
/**Could use Collection<? extends Object> and that is the better choice.
* But I am doing this to illustrate how to use DownCastCollection. **/
public static void print(Collection<Object> col){
for(Object obj : col){
System.out.println(obj);
}
}
public static void main(String[] args){
ArrayList<String> list = new ArrayList<>();
list.addAll(Arrays.asList("a","b","c"));
print(new DownCastCollection<Object>(list));
}
現在是 class:
import java.util.AbstractCollection;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;
public class DownCastCollection<E> extends AbstractCollection<E> implements Collection<E> {
private Collection<? extends E> delegate;
public DownCastCollection(Collection<? extends E> delegate) {
super();
this.delegate = delegate;
}
@Override
public int size() {
return delegate ==null ? 0 : delegate.size();
}
@Override
public boolean isEmpty() {
return delegate==null || delegate.isEmpty();
}
@Override
public boolean contains(Object o) {
if(isEmpty()) return false;
return delegate.contains(o);
}
private class MyIterator implements Iterator<E>{
Iterator<? extends E> delegateIterator;
protected MyIterator() {
super();
this.delegateIterator = delegate == null ? null :delegate.iterator();
}
@Override
public boolean hasNext() {
return delegateIterator != null && delegateIterator.hasNext();
}
@Override
public E next() {
if(!hasNext()) throw new NoSuchElementException("The iterator is empty");
return delegateIterator.next();
}
@Override
public void remove() {
delegateIterator.remove();
}
}
@Override
public Iterator<E> iterator() {
return new MyIterator();
}
@Override
public boolean add(E e) {
throw new UnsupportedOperationException();
}
@Override
public boolean remove(Object o) {
if(delegate == null) return false;
return delegate.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
if(delegate==null) return false;
return delegate.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean removeAll(Collection<?> c) {
if(delegate == null) return false;
return delegate.removeAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
if(delegate == null) return false;
return delegate.retainAll(c);
}
@Override
public void clear() {
if(delegate == null) return;
delegate.clear();
}
}
該問題已被正確識別為與差異相關,但詳細信息不正確。 純函數列表是協變數據函子,這意味着如果類型 Sub 是 Super 的子類型,則 Sub 列表肯定是 Super 列表的子類型。
然而,列表的可變性並不是這里的基本問題。 問題是一般的可變性。 這個問題眾所周知,被稱為協方差問題,我認為它首先是由 Castagna 發現的,它徹底徹底地破壞了 object 作為一般范式的方向。 它基於先前由 Cardelli 和 Reynolds 建立的方差規則。
有點過分簡化,讓我們考慮將類型 T 的 object B 分配給類型 T 的 object A 作為突變。 這不失一般性:A 的一個突變可以寫成 A = f (A) 其中 f: T -> T。當然,問題是雖然函數在它們的余域中是協變的,但它們在它們的域中是逆變的域,但是對於賦值,域和輔域是相同的,所以賦值是不變的!
概括地說,子類型不能突變。 但是 object 方向突變是基本的,因此 object 方向本質上是有缺陷的。
這是一個簡單的例子:在純函數設置中,對稱矩陣顯然是一個矩陣,它是一個子類型,沒問題。 現在讓我們向矩陣添加在坐標 (x,y) 處設置單個元素的功能,並遵循其他元素不變的規則。 現在對稱矩陣不再是子類型,如果你改變 (x,y) 你也改變了 (y,x)。 函數運算是 delta:Sym -> Mat,如果你改變一個對稱矩陣的一個元素,你會得到一個一般的非對稱矩陣。 因此,如果您在 Mat 中包含“更改一個元素”方法,則 Sym 不是子類型。 事實上..幾乎可以肯定沒有合適的子類型。
用更簡單的術語來說:如果你有一個通用數據類型,具有廣泛的變異器,這些變異器利用了它的通用性,你可以確定任何適當的子類型都不可能支持所有這些變異:如果可以的話,它將與超類型,與“適當的”子類型的規范相反。
Java 阻止子類型化可變列表的事實未能解決真正的問題:為什么你使用面向 object 的垃圾,比如 Java 幾十年前它就被抹黑了?
無論如何,這里有一個合理的討論:
https://en.wikipedia.org/wiki/Covariance_and_contravariance_(計算機科學)
讓我們以 JavaSE 教程中的示例為例
public abstract class Shape {
public abstract void draw(Canvas c);
}
public class Circle extends Shape {
private int x, y, radius;
public void draw(Canvas c) {
...
}
}
public class Rectangle extends Shape {
private int x, y, width, height;
public void draw(Canvas c) {
...
}
}
那么為什么狗的列表(圓圈)不應該被隱式地認為是動物的列表(形狀)是因為這種情況:
// drawAll method call
drawAll(circleList);
public void drawAll(List<Shape> shapes) {
shapes.add(new Rectangle());
}
所以 Java“建築師”有 2 個選項可以解決這個問題:
不要認為子類型是隱含的超類型,並給出編譯錯誤,就像現在發生的那樣
將子類型視為它的超類型並在編譯“添加”方法時進行限制(因此在 drawAll 方法中,如果將傳遞圓列表、形狀的子類型,編譯器應該檢測到並限制你執行編譯錯誤那)。
出於顯而易見的原因,它選擇了第一種方式。
我們還應該考慮編譯器如何威脅泛型類:每當我們填充泛型 arguments 時“實例化”一個不同的類型。
因此,我們有ListOfAnimal
、 ListOfDog
、 ListOfCat
等,它們是不同的類,當我們指定泛型 arguments 時,它們最終由編譯器“創建”。這是一個平面層次結構(實際上關於List
根本不是層次結構) .
協變在泛型類的情況下沒有意義的另一個論點是所有類在基礎上都是相同的——都是List
實例。 通過填充通用參數來專門化List
不會擴展 class,它只是讓它適用於該特定的通用參數。
問題已經很清楚了。 但是有一個解決方案; make doSomething通用的:
<T extends Animal> void doSomething<List<T> animals) {
}
現在您可以使用 List<Dog> 或 List<Cat> 或 List<Animal> 調用 doSomething。
另一種解決方案是建立一個新列表
List<Dog> dogs = new ArrayList<Dog>();
List<Animal> animals = new ArrayList<Animal>(dogs);
animals.add(new Cat());
進一步 Jon Skeet 的回答,它使用了這個示例代碼:
// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements List
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?
在最深層次上,這里的問題是dogs
和animals
共享一個參考。 這意味着完成這項工作的一種方法是復制整個列表,這會破壞引用相等性:
// This code is fine
List<Dog> dogs = new ArrayList<Dog>();
dogs.add(new Dog());
List<Animal> animals = new ArrayList<>(dogs); // Copy list
animals.add(new Cat());
Dog dog = dogs.get(0); // This is fine now, because it does not return the Cat
調用List<Animal> animals = new ArrayList<>(dogs);
,您不能隨后直接將animals
分配dogs
或cats
:
// These are both illegal
dogs = animals;
cats = animals;
因此,您不能將錯誤的Animal
子類型放入列表中,因為沒有錯誤的子類型——任何 object 子類型? extends Animal
? extends Animal
可以添加到animals
。
顯然,這改變了語義,因為列表animals
和dogs
不再共享,所以添加到一個列表不會添加到另一個列表(這正是你想要的,以避免將Cat
添加到列表中的問題應該只包含Dog
對象)。 此外,復制整個列表可能效率低下。 但是,這確實通過打破引用相等性解決了類型等價問題。
我看到這個問題已經回答了很多次,只是想就同一個問題發表我的意見。
讓我們 go 向前創建一個簡化的 Animal class 層次結構。
abstract class Animal {
void eat() {
System.out.println("animal eating");
}
}
class Dog extends Animal {
void bark() { }
}
class Cat extends Animal {
void meow() { }
}
現在讓我們看看我們的老朋友 Arrays,我們知道它隱式支持多態性——
class TestAnimals {
public static void main(String[] args) {
Animal[] animals = {new Dog(), new Cat(), new Dog()};
Dog[] dogs = {new Dog(), new Dog(), new Dog()};
takeAnimals(animals);
takeAnimals(dogs);
}
public void takeAnimals(Animal[] animals) {
for(Animal a : animals) {
System.out.println(a.eat());
}
}
}
class 編譯正常,當我們運行上面的 class 時,我們得到 output
animal eating
animal eating
animal eating
animal eating
animal eating
animal eating
這里要注意的一點是,takeAnimals() 方法被定義為接受 Animal 類型的任何東西,它可以接受 Animal 類型的數組,也可以接受 Dog 的數組,因為 Dog-is-a-Animal。 所以這就是多態性在起作用。
現在讓我們對 generics 使用同樣的方法,
現在假設我們稍微調整一下代碼並使用 ArrayLists 而不是 Arrays -
class TestAnimals {
public static void main(String[] args) {
ArrayList<Animal> animals = new ArrayList<Animal>();
animals.add(new Dog());
animals.add(new Cat());
animals.add(new Dog());
takeAnimals(animals);
}
public void takeAnimals(ArrayList<Animal> animals) {
for(Animal a : animals) {
System.out.println(a.eat());
}
}
}
上面的 class 將編譯並生成 output -
animal eating
animal eating
animal eating
animal eating
animal eating
animal eating
所以我們知道這是有效的,現在讓我們稍微調整一下這個 class 以多態地使用 Animal 類型 -
class TestAnimals {
public static void main(String[] args) {
ArrayList<Animal> animals = new ArrayList<Animal>();
animals.add(new Dog());
animals.add(new Cat());
animals.add(new Dog());
ArrayList<Dog> dogs = new ArrayList<Dog>();
takeAnimals(animals);
takeAnimals(dogs);
}
public void takeAnimals(ArrayList<Animal> animals) {
for(Animal a : animals) {
System.out.println(a.eat());
}
}
}
看起來編譯上面的 class 應該沒有問題,因為 takeAnimals() 方法被設計為接受 Animal 和 Dog-is-a-Animal 類型的任何 ArrayList,所以它在這里不應該是一個交易破壞者。
但是,不幸的是,編譯器拋出一個錯誤,不允許我們將 Dog ArrayList 傳遞給需要 Animal ArrayList 的變量。
你問為什么?
因為試想一下,如果 JAVA 允許 Dog ArrayList - dogs - 被放入 Animal ArrayList - animals - 然后在 takeAnimals() 方法中有人做了類似的事情 -
animals.add(new Cat());
認為這應該是可行的,因為理想情況下它是一個動物 ArrayList,你應該在 position 中添加任何貓作為貓也是動物,但實際上你傳遞了一個狗類型 ArrayList 給它。
所以,現在你一定在想同樣的事情也應該發生在 Arrays 上。 你這樣想是對的。
如果有人試圖用 Arrays 做同樣的事情,那么 Arrays 也會拋出一個錯誤,但是 Arrays 在運行時處理這個錯誤,而 ArrayLists 在編譯時處理這個錯誤。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.