fro*_*die 727 java generics polymorphism inheritance
我对Java泛型如何处理继承/多态性感到困惑.
假设以下层次结构 -
动物(父母)
狗 - 猫(儿童)
所以假设我有一个方法doSomething(List<Animal> animals).根据所有继承和多态的规则,我会假设a List<Dog> 是 a List<Animal>而a List<Cat> 是 a List<Animal>- 所以任何一个都可以传递给这个方法.不是这样.如果我想实现这种行为,我必须明确告诉该方法接受一个Animal的任何子类的列表doSomething(List<? extends Animal> animals).
我知道这是Java的行为.我的问题是为什么?为什么多态通常是隐含的,但是当涉及泛型时必须指定它?
Jon*_*eet 870
不,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?
Run Code Online (Sandbox Code Playgroud)
突然间你有一只非常困惑的猫.
现在,您无法添加Cat到a,List<? extends Animal>因为您不知道它是a List<Cat>.您可以检索一个值,并知道它将是一个Animal,但您不能添加任意动物.反之亦然List<? super Animal>- 在这种情况下,您可以Animal安全地添加一个,但您不知道从它可以检索到什么,因为它可能是一个List<Object>.
Mic*_*yan 44
a List<Dog>不是a 的原因是List<Animal>,例如,你可以插入Cata List<Animal>,但不能插入List<Dog>...你可以使用通配符使generics在可能的情况下更具可扩展性; 例如,从a读取List<Dog>类似于读取List<Animal>- 但不是写作.
在Java语言的泛型和从Java教程泛型科有一个很好的,深入的解释,为什么有些东西是或不是多晶型或泛型允许的.
ein*_*ica 34
List<Dog>不是Java中的一个List<Animal>
这也是事实
狗的清单是 - 英文动物清单(嗯,在合理的解释下)
OP的直觉起作用的方式 - 当然是完全有效的 - 是后一句.但是,如果我们应用这种直觉,我们会在其类型系统中获得一种非Java风格的语言:假设我们的语言允许将猫添加到我们的狗列表中.那是什么意思?这意味着该名单不再是狗的名单,而只是一个动物名单.以及一系列哺乳动物和一系列四足动物.
换句话说:List<Dog>Java中的A 并不是指英语中的"狗列表",它的意思是"可以有狗的列表,而不是其他".
更一般地说,OP的直觉适用于对象的操作可以改变其类型的语言,或者更确切地说,对象的类型是其值的(动态)函数.
Yis*_*hai 32
我会说泛型的全部意义在于它不允许这样做.考虑数组的情况,它允许这种类型的协方差:
Object[] objects = new String[10];
objects[0] = Boolean.FALSE;
Run Code Online (Sandbox Code Playgroud)
该代码编译良好,但抛出运行时错误(java.lang.ArrayStoreException: java.lang.Boolean在第二行).它不是类型安全的.泛型的要点是添加编译时类型安全性,否则你可以坚持使用没有泛型的普通类.
现在有你需要更灵活倍,这是什么? super Class和? extends Class是.前者是需要插入类型Collection(例如)时,后者是需要以类型安全的方式从中读取时.但同时做两者的唯一方法就是拥有一种特定的类型.
out*_*dev 12
要理解这个问题,与数组进行比较是有用的.
List<Dog>是不是子类List<Animal>.
但是 Dog[] 是子类Animal[].
数组是可恢复的和协变的.
可恢复意味着它们的类型信息在运行时完全可用.
因此,数组提供运行时类型安全性但不提供编译时类型安全性.
// 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
Run Code Online (Sandbox Code Playgroud)
对于泛型,反之亦然:
泛型被擦除且不变.
因此泛型不能提供运行时类型安全性,但它们提供编译时类型安全性.
在下面的代码中,如果泛型是协变的,那么就有可能在第3行产生堆污染.
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
animals.add(new Cat());
Run Code Online (Sandbox Code Playgroud)
Mik*_*kis 10
其他人已经很好地解释了为什么不能将后代列表转换为超类列表。
然而,许多人访问这个问题寻找解决方案。
所以,从Java 10版本开始这个问题的解决方案如下:
(注:S = 超类)
List<S> supers = List.copyOf( descendants );
Run Code Online (Sandbox Code Playgroud)
如果完全安全,则此函数将执行强制转换;如果强制转换不安全,则执行复制。结果列表是不可修改的。
有关深入的解释(考虑到此处其他答案提到的潜在陷阱),请参阅相关问题和我的 2022 年答案:https ://stackoverflow.com/a/72195980/773113
这里给出的答案并没有完全说服我。所以相反,我再举一个例子。
public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
consumer.accept(supplier.get());
}
Run Code Online (Sandbox Code Playgroud)
听起来不错,不是吗?但是你只能为s传递Consumers 和s。如果您有消费者,但有供应商,则尽管两者都是动物,但它们不应该适合。为了禁止这种情况,增加了额外的限制。SupplierAnimalMammalDuck
代替上面的,我们必须定义我们使用的类型之间的关系。
例如,
public <A extends Animal> void passOn(Consumer<A> consumer, Supplier<? extends A> supplier) {
consumer.accept(supplier.get());
}
Run Code Online (Sandbox Code Playgroud)
确保我们只能使用为消费者提供正确类型的对象的供应商。
OTOH,我们也可以
public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<A> supplier) {
consumer.accept(supplier.get());
}
Run Code Online (Sandbox Code Playgroud)
反过来说:我们定义了 的类型Supplier并限制它可以放入Consumer.
我们甚至可以做到
public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<? extends A> supplier) {
consumer.accept(supplier.get());
}
Run Code Online (Sandbox Code Playgroud)
其中,具有直观的关系Life- > Animal- > Mammal- > Dog,Cat等等,我们甚至可以在一Mammal成Life消费者,而不是String到Life消费者。
这种行为的基本逻辑是Generics遵循类型擦除的机制。因此,在运行时,您无法确定没有此类擦除过程的不collection相似类型arrays。所以回到你的问题...
因此,假设有一种如下所示的方法:
add(List<Animal>){
//You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism
}
Run Code Online (Sandbox Code Playgroud)
现在,如果Java允许调用者将Animal类型的List添加到此方法中,那么您可能会将错误的内容添加到集合中,并且在运行时由于类型擦除它也会运行。在使用数组的情况下,您会在此类情况下获得运行时异常...
因此,从本质上讲,这种行为是可以实现的,因此不能将错误的东西添加到集合中。现在,我相信存在类型擦除,以便与不带泛型的传统Java兼容。
子类型对于参数化类型来说是不变的。即使该类Dog是 的子类型Animal,参数化类型List<Dog>也不是 的子类型List<Animal>。相反,数组使用协变子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);
Run Code Online (Sandbox Code Playgroud)
正如 @Jon Skeet 所说,这段代码是非法的,因为否则它会在狗期望的时候返回猫,从而违反类型约束。
将上面的代码与数组的类似代码进行比较是有启发性的。
Dog[] dogs = new Dog[1];
Object[] animals = dogs;
animals[0] = new Cat(); // run-time error
Dog dog = dogs[0];
Run Code Online (Sandbox Code Playgroud)
代码是合法的。但是,会引发数组存储异常。数组在运行时携带其类型,这样 JVM 就可以强制执行协变子类型的类型安全。
为了进一步理解这一点,让我们看看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");
}
}
Run Code Online (Sandbox Code Playgroud)
使用命令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
}
Run Code Online (Sandbox Code Playgroud)
观察方法体的翻译代码是相同的。编译器通过擦除来替换每个参数化类型。此属性至关重要,意味着它不会破坏向后兼容性。
总之,参数化类型不可能实现运行时安全,因为编译器通过擦除来替换每个参数化类型。这使得参数化类型只不过是语法糖。
| 归档时间: |
|
| 查看次数: |
103795 次 |
| 最近记录: |