Is List<Dog> a subclass of List<Animal>? Why are Java generics not implicitly polymorphic?
对于Java泛型如何处理继承/多态,我有点困惑。
采用以下层次结构-
动物(亲本)
狗-猫(儿童)
所以假设我有一个方法
我知道这是Java的行为。我的问题是为什么?为什么多态性通常是隐式的,但当涉及到泛型时,必须指定它?
不,
1 2 3 4 5 | // 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? |
突然你有一只非常困惑的猫。
现在,您不能将
您要查找的是所谓的协变类型参数。这意味着,如果一种方法中的一种对象可以替换另一种对象(例如,
添加语法以允许将类型参数指定为协变,这将很有用,因为这样可以避免方法声明中的
Java语言中的泛型和Java教程中的泛型部分有一个非常好的、深入的解释,说明为什么有些事物是多态的或不允许泛型的。
我要说的是,仿制药的关键是它不允许这样做。考虑数组的情况,数组确实允许这种协方差:
该代码编译良好,但在第二行中抛出了一个运行时错误(
现在有些时候你需要更加灵活,这就是
我认为应该在其他答案中加上一点,那就是
List isn't-aList in Java
这也是真的
A list of dogs is-a list of animals in English (well, under a reasonable interpretation)
欧普的直觉运作的方式——当然是完全有效的——是后一句话。然而,如果我们运用这种直觉,我们就可以得到一个不是Java系统的语言——它的类型系统:假设我们的语言允许把猫添加到我们的狗的列表中。这意味着什么?这意味着名单不再是狗的名单,而仅仅是动物的名单。还有哺乳动物的名单,还有四足动物的名单。
换言之,在Java中EDCOX1的0个词并不意味着英语中的"狗的列表",意思是"一个可以有狗的列表,而不是其他任何东西"。
更一般地说,op的直觉倾向于一种语言,在这种语言中,对象的操作可以改变其类型,或者更确切地说,对象的类型是其值的(动态)函数。
为了理解这个问题,与数组进行比较是很有用的。
数组是可重写的和协变的。可重设意味着它们的类型信息在运行时是完全可用的。因此,数组提供运行时类型安全性,但不提供编译时类型安全性。
1 2 3 4 | // 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行造成堆污染。
1 2 3 | List<Dog> dogs = new ArrayList<>(); List<Animal> animals = dogs; // compile-time error, otherwise heap pollution animals.add(new Cat()); |
这种行为的基本逻辑是
所以假设有一种方法,如下所示:
1 2 3 | add(List<Animal>){ //You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism } |
现在,如果Java允许调用方向该方法添加类型的动物列表,那么您可能会将错误的东西添加到集合中,并且在运行时也会由于类型擦除而运行。而在数组的情况下,您将获得此类场景的运行时异常…
因此,在本质上,这种行为是实现的,这样就不能向集合中添加错误的内容。现在我相信存在类型擦除,以便在没有泛型的情况下提供与遗留Java的兼容性。
这里给出的答案并没有完全说服我。因此,我做了另一个例子。
1 2 3 | public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) { consumer.accept(supplier.get()); } |
听起来不错,不是吗?但是你只能通过
我们必须定义我们使用的类型之间的关系,而不是上面所说的。
例如:
1 2 3 | public <A extends Animal> void passOn(Consumer<A> consumer, Supplier<? extends A> supplier) { consumer.accept(supplier.get()); } |
确保我们只能使用为消费者提供正确对象类型的供应商。
噢,我们也可以
1 2 3 | public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<A> supplier) { consumer.accept(supplier.get()); } |
另一种方法是:定义
我们甚至可以做到
1 2 3 | public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<? extends A> supplier) { consumer.accept(supplier.get()); } |
其中,由于
实际上,你可以使用一个接口来实现你想要的。
1 2 3 4 5 6 7 8 9 |
}
然后可以使用集合
1 2 |
如果您确定列表项是给定超级类型的子类,则可以使用此方法强制转换列表:
1 | (List<Animal>) (List<?>) dogs |
当您想在构造函数中传递列表或在其上迭代时,这非常有用。
子类型对于参数化类型是不变的。更为困难的是,类
不变子类型确保不受Java强制执行的类型约束。考虑@jon skeet给出的以下代码:
1 2 3 4 | 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所述,此代码是非法的,因为否则它将违反类型约束,在需要狗时返回cat。
将上面的代码与数组的类似代码进行比较是有指导意义的。
1 2 3 4 | Dog[] dogs = new Dog[1]; Object[] animals = dogs; animals[0] = new Cat(); // run-time error Dog dog = dogs[0]; |
该准则是合法的。但是,引发数组存储异常。数组在运行时携带其类型,这样JVM就可以强制协变子类型的类型安全性。
为了进一步了解这一点,让我们看看下面这个类的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
使用命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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 } |
注意方法体的翻译代码是相同的。编译器用它的擦除替换了每个参数化类型。这一属性至关重要,意味着它不会破坏向后兼容性。
总之,对于参数化类型来说,运行时安全性是不可能的,因为编译器用它的擦除来替换每个参数化类型。这使得参数化类型只不过是语法上的糖分。
答案和其他答案都是正确的。我将用我认为有帮助的解决方案来补充这些答案。我认为这经常出现在编程中。需要注意的一点是,对于集合(列表、集合等),主要问题是添加到集合中。这就是事情的崩溃之处。即使移除也可以。
在大多数情况下,我们可以使用
1 2 3 4 5 6 7 8 9 10 11 12 13 | /**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)); } |
现在同学们:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | 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(); } |
}
另一个解决方案是建立一个新的列表
1 2 3 | List<Dog> dogs = new ArrayList<Dog>(); List<Animal> animals = new ArrayList<Animal>(dogs); animals.add(new Cat()); |
让我们以Javase教程中的示例为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
因此,为什么不应将狗(圈)列表隐式地视为动物(形状)列表是因为这种情况:
1 2 3 4 5 6 7 | // drawAll method call drawAll(circleList); public void drawAll(List<Shape> shapes) { shapes.add(new Rectangle()); } |
所以Java"架构师"有2个选项来解决这个问题:
不要认为子类型隐式地是它的父类型,并给出编译错误,就像现在发生的那样。
将子类型视为它的父类型,并在编译时限制"添加"方法(因此,在drawall方法中,如果将传递圆列表、形状的子类型,则编译器应检测到并限制您使用编译错误执行此操作)。
出于明显的原因,选择了第一种方式。
我们还应该考虑编译器如何威胁泛型类:在"实例化"中,每当我们填充泛型参数时,都会使用不同的类型。
因此,我们有
对于泛型类,协方差没有意义的另一个论点是,在基本上所有类都是相同的——都是
问题已经被很好地识别出来了。但是有一个解决方案;使剂量测量通用:
1 2 | <T extends Animal> void doSomething<List<T> animals) { } |
现在您可以使用list