List <Dog>是List <Animal>的一个子类吗? 为什么不是Java的generics隐式多态?
我对Javagenerics如何处理inheritance/多态性有些困惑。
假设以下层次结构 –
动物 (家长)
狗 – 猫 (儿童)
所以假设我有一个方法doSomething(List<Animal> animals)
。 按照所有inheritance和多态的规则,我会假定一个List<Dog>
是一个List<Animal>
,一个List<Cat>
是一个List<Animal>
– 所以任何一个都可以传递给这个方法。 不是这样。 如果我想实现这种行为,我必须明确地告诉方法通过说doSomething(List<? extends Animal> animals)
接受动物子集的doSomething(List<? extends Animal> animals)
。
我明白这是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>
。
你在找什么叫做协变types参数 。 问题是它们在一般情况下不是types安全的,特别是对于可变列表。 假设你有一个List<Dog>
,它可以作为一个List<Animal>
。 当你试图添加一个Cat到这个List<Animal>
这真的是一个List<Dog>
什么? 自动允许types参数是协变的,因此打破了types系统。
添加语法以允许将types参数指定为协变是有用的,这避免了? extends Foo
在方法声明中? extends Foo
,但确实增加了额外的复杂性。
List<Dog>
不是List<Animal>
是,例如,您可以将Cat
插入到List<Animal>
,但不能插入到List<Dog>
…您可以使用通配符generics在可能的情况下更具可扩展性; 例如,从一个List<Dog>
中读取类似于从List<Animal>
读取 – 而不是写入。
Java语言中的generics和Javagenerics中的generics 部分对于为什么某些事物是多态的或者generics允许的有一个非常好的深入的解释。
generics的全部意义在于它不允许这样做。 考虑数组的情况,它允许这种types的协方差:
Object[] objects = new String[10]; objects[0] = Boolean.FALSE;
该代码编译好,但会引发运行时错误(第二行java.lang.ArrayStoreException: java.lang.Boolean
)。 这不是types安全的。 generics的要点是增加编译时的types安全性,否则你可以坚持一个没有generics的普通类。
现在有些时候你需要更加灵活,那是什么? super Class
? super Class
和? extends Class
? extends Class
是为了。 前者是当你需要插入到一个typesCollection
(例如),而后者是当你需要从中读取,types安全的方式。 但同时做这两件事的唯一方法是有一个特定的types。
我认为应该加上其他 答案中提到的一点
在Java中
List<Dog>
不是一个List<Animal>
这也是事实
狗的名单是一个英文的动物列表(以及合理的解释)
OP的直觉工作方式 – 当然是完全有效的 – 是后一句话。 但是,如果我们运用这个直觉,我们就会得到一个types系统中不是Java的语言:假设我们的语言允许在我们的狗列表中添加一个猫。 那是什么意思? 这意味着名单不再是狗的名单,而只是一个动物名单。 还有一张哺乳动物的名单,还有一张四angular形的名单。
换句话说,Java中的List<Dog>
并不是英文中的“狗的列表”,而是指“可以有狗的列表,而不是其他的”。
更一般地说, OP的直觉适用于一种语言,在这种语言中,对象的操作可以改变它的types ,或者说,对象的types是它的价值的(dynamic的)function。
这种行为的基本逻辑是Generics
遵循types擦除机制。 所以在运行时,如果要确定types的collection
而不像arrays
那样没有这样的擦除过程,那么你就没有办法。 所以回到你的问题…
所以假设有一个方法如下:
add(List<Animal>){//You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism}
现在,如果Java允许调用者添加types的dynamic列表到这个方法,那么你可能会添加错误的东西到收集和运行时也会运行,因为types擦除。 而在数组的情况下,你会得到一个运行时exception这种情况下…
因此,本质上这个行为是被实现的,所以不能把错误的东西添加到集合中。 现在我相信types擦除存在,以便与没有generics的遗留Java兼容….
这里给出的答案并没有完全说服我。 相反,我做了另一个例子。
public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) { consumer.accept(supplier.get()); }
听起来不错,不是吗? 但你只能通过Consumer
和Supplier
的Animal
。 如果你有一个Mammal
消费者,但一个Duck
供应商,他们不应该适合,虽然都是动物。 为了禁止这一点,已经增加了额外的限制。
而不是上述,我们必须定义我们使用的types之间的关系。
例如,
public <A extends Animal> void passOn(Consumer<A> consumer, Supplier<? extends A> supplier) { consumer.accept(supplier.get()); }
确保我们只能使用为我们提供正确types的消费者对象的供应商。
OTOH,我们也可以做
public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<A> supplier) { consumer.accept(supplier.get()); }
我们从另一个angular度去考虑:我们定义Supplier
的types,并限制它可以放入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
消费者,而不是一个Life
消费者的String
。
其实你可以使用一个界面来实现你想要的。
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());
如果您确定列表项是给定超types的子类,则可以使用以下方法来转换列表:
(List<Animal>) (List<?>) dogs
当您想要在构造函数中传递列表或迭代它时,这是有用的
要理解这个问题,比较数组是有用的。
List<Dog>
不是 List<Animal>
子类。
但是 Dog[]
是 Animal[]
子类。
数组是可确定的和协变的 。
Reifiable意味着它们的types信息在运行时完全可用。
因此,数组提供了运行时types的安全性,但不是编译时types的安
// 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不能提供运行时types的安全性,但它们提供编译时types的安全性。
在下面的代码中,如果generics是协变的,那么在第3行就可能造成堆污染 。
List<Dog> dogs = new ArrayList<>(); List<Animal> animals = dogs; // compile-time error, otherwise heap pollution animals.add(new Cat());
让我们以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“架构师”有两个选项可以解决这个问题:
-
不要认为子types是隐式的,它是超types的,给出一个编译错误,就像现在发生的那样
-
考虑子types是超types,并限制在编译“add”方法(所以在drawAll方法中,如果一个圆的列表,子types的forms将被传递,编译器应该检测到并限制你编译错误到做那)。
出于显而易见的原因,select了第一种方式。
答案以及其他答案是正确的。 我将添加一个解决scheme,我认为这将有所帮助。 我觉得这经常在编程中出现。 有一点需要注意的是,collections(列表,集合等)的主要问题是添加到collections。 这是事情发生的地方。 即使删除也行。
在大多数情况下,我们可以使用Collection<? extends T>
Collection<? extends T>
而不是Collection<T>
,那应该是第一select。 但是,我发现这样的情况并不容易。 这是否是最好的事情呢? 我在这里介绍一个类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(); }
}