我对Java泛型如何处理继承/多态感到困惑。
假设以下层次结构-
动物 (父母)
狗 - 猫 (儿童)
因此,假设我有一个方法doSomething(List
。 根据继承和多态性的所有规则,我将假定List
是 List
而List
是 List
-因此可以将任何一个传递给此方法。 不是这样 如果要实现此行为,则必须通过说doSomething(List extends Animal> animals)
来明确告诉该方法接受Animal的任何子类的doSomething(List extends Animal> animals)
。
我了解这是Java的行为。 我的问题是为什么 ? 为什么多态性通常是隐式的,但是当涉及泛型时,必须指定它?
这种行为的基本逻辑是Generics
遵循类型擦除的机制。 因此,在运行时,您无法确定与没有这种擦除过程的arrays
不同的collection
的类型。 所以回到你的问题...
因此,假设有一种如下所示的方法:
add(List){
//You can add List and this will compile as per rules of polymorphism
}
现在,如果Java允许调用者将Animal类型的List添加到此方法中,则可能会将错误的内容添加到集合中,并且在运行时由于类型擦除它也会运行。 在使用数组的情况下,您会在此类情况下获得运行时异常...
因此,从本质上讲,这种行为是可以实现的,因此不能将错误的东西添加到集合中。 现在,我相信存在类型擦除,以便与不带泛型的传统Java兼容。
我认为应该在其他 答案中提到的一点是
List
不是-Java中的List
的确如此
狗的清单是英语的动物清单(在合理的解释下)
OP的直觉的工作方式(当然完全有效)是后一句话。 但是,如果我们采用这种直觉,则会得到一种在其类型系统中不是Java风格的语言:假设我们的语言确实允许将猫添加到我们的狗列表中。 那是什么意思? 这将意味着该列表不再是狗的列表,而仅是动物的列表。 还有哺乳动物清单和四足动物清单。
换句话说,Java中的List
并不意味着英语中的“狗列表”,而是“可以有狗的列表,别无其他”。
更一般而言, OP的直觉倾向于一种语言,在该语言上,对对象的操作可以更改其类型 ,或者更确切地说,对象的类型是其值的(动态)函数。
不, List
不是 List
。 考虑使用List
可以做什么-您可以向其中添加任何动物...包括猫。 现在,您可以在逻辑上将猫添加到一窝小狗中吗? 绝对不。
// Illegal code - because otherwise life would be Bad
List dogs = new ArrayList(); // ArrayList implements List
List 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
。 您可以检索一个值并知道它将是Animal
,但是不能添加任意动物。 相反,对于List super Animal>
List super Animal>
-在那种情况下,您可以安全地向其添加Animal
,但是您可能对List
不了解,因为它可能是List
。
您正在寻找的被称为协变类型参数 。 这意味着如果一种对象类型可以在方法中替代另一种对象(例如, Animal
可以用Dog
替换),则同样适用于使用这些对象的表达式(因此List
可以替换为List
)。 问题在于,协方差通常对于可变列表并不安全。 假设您有一个List
,并且它被用作List
。 当您尝试将猫添加到此List
(实际上是List
什么? 自动允许类型参数协变会破坏类型系统。
添加语法以允许将类型参数指定为协变将很有用,这样可以避免使用? extends Foo
在方法声明中? extends Foo
,但这确实增加了额外的复杂性。
List
不是List
是,例如,您可以将Cat
插入List
,但不能插入List
...您可以使用通配符泛型在可能的情况下更可扩展; 例如,从List
读取与从List
读取相似-但不能写入。
Java语言中的泛型以及Java教程中的泛型部分对为什么某些事物是多态的或不允许使用泛型的,进行了很好的深入解释。
我想说泛型的全部要点是它不允许这样做。 考虑一下数组的情况,它确实允许这种类型的协方差:
Object[] objects = new String[10];
objects[0] = Boolean.FALSE;
该代码可以正常编译,但是会引发运行时错误(第二行中是java.lang.ArrayStoreException: java.lang.Boolean
)。 它不是类型安全的。 泛型的要点是增加编译时类型的安全性,否则您可以坚持使用没有泛型的普通类。
现在有时候您需要更加灵活,那是什么? super Class
? super Class
和? extends Class
? extends Class
是为。 前者是当您需要插入类型Collection
(例如)时,后者是当您需要以类型安全的方式从中读取时。 但是,同时执行这两个操作的唯一方法是具有特定的类型。
答案以及其他答案都是正确的。 我将使用我认为会有所帮助的解决方案来补充这些答案。 我认为这经常在编程中出现。 需要注意的一件事是,对于集合(列表,集合等),主要问题是添加到集合中。 那是事情崩溃的地方。 即使删除也可以。
在大多数情况下,我们可以使用Collection extends T>
Collection extends T>
而不是Collection
,那应该是首选。 但是,我发现不容易做到这一点的情况。 关于这是否始终是最好的事情,有待辩论。 我在这里展示的是DownCastCollection类,可以将其转换为Collection extends T>
Collection extends T>
到Collection
(我们可以为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
现在上课:
import java.util.AbstractCollection;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;
public class DownCastCollection extends AbstractCollection implements Collection {
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{
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 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();
}
}
这里给出的答案并不能完全说服我。 因此,我再举一个例子。
public void passOn(Consumer consumer, Supplier supplier) {
consumer.accept(supplier.get());
}
听起来不错,不是吗? 但是您只能通过Consumer
和Supplier
的Animal
。 如果您有Mammal
消费者,但有Duck
供应商,尽管它们都是动物,但它们不适合。 为了禁止这种情况,添加了其他限制。
代替上面的方法,我们必须定义使用的类型之间的关系。
例如
public void passOn(Consumer consumer, Supplier extends A> supplier) {
consumer.accept(supplier.get());
}
确保我们只能使用为消费者提供正确类型的对象的供应商。
OTOH,我们也可以做
public void passOn(Consumer super A> consumer, Supplier supplier) {
consumer.accept(supplier.get());
}
我们采用另一种方式:我们定义了Supplier
的类型,并限制了它可以放入Consumer
。
我们甚至可以做
public void passOn(Consumer super A> consumer, Supplier extends A> supplier) {
consumer.accept(supplier.get());
}
在这里,有了Life
-> Animal
-> Mammal
-> Dog
, Cat
等的直观关系,我们甚至可以将Mammal
放入Life
消费者中,而不是将String
放入Life
消费者中。
实际上,您可以使用界面来实现所需的功能。
public interface Animal {
String getName();
String getVoice();
}
public class Dog implements Animal{
@Override
String getName(){return "Dog";}
@Override
String getVoice(){return "woof!";}
}
然后,您可以使用以下集合
List animalGroup = new ArrayList();
animalGroup.add(new Dog());
如果确定列表项是给定超级类型的子类,则可以使用以下方法强制转换列表:
(List) (List>) dogs
当您要在构造函数中传递列表或对其进行迭代时,这很有用
让我们以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 shapes) {
shapes.add(new Rectangle());
}
因此,Java“建筑师”有2个选项可以解决此问题:
不要认为子类型隐式是它的超类型,并给出编译错误,就像现在发生的那样
认为该子类型是其父类型,并限制在编译“ add”方法时使用(因此在drawAll方法中,如果将传递一系列圆形,形状的子类型,则编译器应检测到该情况,并限制您执行编译错误那)。
由于明显的原因,选择了第一种方法。
要了解该问题,与数组进行比较很有用。
List
不是 List
子类。
但是 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
反之,泛型则相反:
泛型被擦除且不变 。
因此,泛型不能提供运行时类型安全性,但它们可以提供编译时类型安全性。
在下面的代码中,如果泛型是协变的,则可能在第3行产生堆污染 。
List dogs = new ArrayList<>();
List animals = dogs; // compile-time error, otherwise heap pollution
animals.add(new Cat());
我们还应考虑编译器如何威胁通用类:在填充通用参数时,“实例化”另一种类型。
因此,我们具有ListOfAnimal
, ListOfDog
, ListOfCat
等,它们是不同的类,最终在指定通用参数时由编译器“创建”。 这是一个简单的层次结构(实际上,与List
根本不是层次结构)。
为什么在泛型类的情况下协方差没有意义的另一个论点是,实际上所有类都是相同的List
实例。 通过填充泛型参数来对List
进行特殊化并不会扩展该类,而只是使它适用于该特定的泛型参数。
该问题已得到充分认识。 但是有一个解决方案。 使doSomething通用:
void doSomething animals) {
}
现在您可以使用List
子类型对于参数化类型是不变的。 甚至Dog
类是Animal
的子类型,参数化类型List
也不是List
的子类型。 相反,数组使用协变子类型,因此数组类型Dog[]
是Animal[]
的子类型。
不变子类型可确保不违反Java强制的类型约束。 考虑@Jon Skeet给出的以下代码:
List dogs = new ArrayList(1);
List animals = dogs;
animals.add(new Cat()); // compile-time error
Dog dog = dogs.get(0);
如@Jon Skeet所述,此代码是非法的,因为否则它将在需要狗的情况下返回猫,从而违反类型约束。
将以上内容与数组的类似代码进行比较很有启发性。
Dog[] dogs = new Dog[1];
Object[] animals = dogs;
animals[0] = new Cat(); // run-time error
Dog dog = dogs[0];
该代码是合法的。 但是,抛出一个数组存储异常 。 数组以这种方式在运行时携带其类型,从而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 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."":()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."":(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."":(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
}
请注意,方法主体的翻译代码相同。 编译器通过擦除来替换每个参数化类型。 此属性至关重要,意味着它不会破坏向后兼容性。
总之,参数化类型无法实现运行时安全,因为编译器会通过擦除来替换每个参数化类型。 这使得参数化类型仅是语法糖。
另一个解决方案是建立一个新列表
List dogs = new ArrayList();
List animals = new ArrayList(dogs);
animals.add(new Cat());
除了Jon Skeet的答案之外,他还使用以下示例代码:
// Illegal code - because otherwise life would be Bad
List dogs = new ArrayList(); // ArrayList implements List
List 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 dogs = new ArrayList();
dogs.add(new Dog());
List 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
,您随后便无法直接将animals
分配dogs
或cats
:
// These are both illegal
dogs = animals;
cats = animals;
因此您不能将错误的Animal
子类型放入列表中,因为没有错误的子类型-任何子类型的对象? extends Animal
? extends Animal
可以被添加到animals
。
显然,这改变了语义,因为不再共享列表中的animals
和dogs
,因此添加到一个列表中不会添加到其他列表中(这正是您想要的,以避免将Cat
添加到列表中的问题只能包含Dog
对象)。 同样,复制整个列表可能效率很低。 但是,这确实可以通过打破引用相等来解决类型对等问题。