首页 文章

List <Dog>是List <Animal>的子类吗?为什么Java泛型不是隐式多态的?

提问于
浏览
654

关于Java泛型如何处理继承/多态,我有点困惑 .

假设以下层次结构 -

Animal (家长)

Dog - Cat (儿童)

所以假设我有一个方法 doSomething(List<Animal> animals) . 通过所有继承和多态的规则,我会假设 List<Dog>List<Animal>List<Cat>List<Animal> - 因此任何一个都可以传递给这个方法 . 不是这样 . 如果我想实现这种行为,我必须通过说 doSomething(List<? extends Animal> animals) 明确告诉方法接受Animal的任何子类的列表 .

我知道这是Java的行为 . 我的问题是为什么?为什么多态通常是隐含的,但是当涉及泛型时必须指定它?

16 回答

  • 1

    您正在寻找的是covariant type参数 . 这意味着如果一种类型的对象可以替换方法中的另一种类型(例如, Animal 可以替换为 Dog ),则同样适用于使用这些对象的表达式(因此 List<Animal> 可以替换为 List<Dog> ) . 问题是协方差一般对于可变列表是不安全的 . 假设你有 List<Dog> ,它被用作 List<Animal> . 当您尝试将Cat添加到 List<Animal> 时会发生什么?这真的是 List<Dog> ?自动允许类型参数协变会破坏类型系统 .

    添加语法以允许将类型参数指定为协变是有用的,这避免了方法声明中的 ? extends Foo ,但这确实增加了额外的复杂性 .

  • 70

    我们还应该考虑编译器如何威胁泛型类:每当我们填充泛型参数时,“实例化”一个不同的类型 .

    因此,我们有 ListOfAnimalListOfDogListOfCat 等,这些是我们指定泛型参数时编译器最终成为"created"的不同类 . 这是一个扁平的层次结构(实际上关于 List 根本不是一个层次结构) .

    在泛型类的情况下,为什么协方差没有意义的另一个论点是,在所有类都相同的情况下是 List 实例 . 通过填充泛型参数来专门化 List 不会扩展类,它只是使它适用于该特定的泛型参数 .

  • 29

    如果您确定列表项是给定超类型的子类,则可以使用以下方法强制转换列表:

    (List<Animal>) (List<?>) dogs
    

    当您想要在构造函数中传递列表或迭代它时,这非常有用

  • 0

    不, List<Dog> 不是 List<Animal> . 考虑一下你可以用_268829做什么 - 你可以添加任何动物......包括一只猫 . 现在,你可以在逻辑上将一只猫添加到一窝幼犬吗?绝对不 .

    // 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> ,因为您没有't know it'是 List<Cat> . 您可以检索一个值并知道它将是 Animal ,但您无法添加任意动物 . List<? super Animal> 反之亦然 - 在这种情况下,您可以安全地添加 Animal ,但您不知道从中检索到什么,因为它可能是 List<Object> .

  • 795

    这个问题已得到很好的证实 . 但是有一个解决方案; make doSomething generic:

    <T extends Animal> void doSomething<List<T> animals) {
    }
    

    现在,您可以使用List <Dog>或List <Cat>或List <Animal>调用doSomething .

  • 4

    answer以及其他答案都是正确的 . 我将使用我认为有用的解决方案来添加这些答案 . 我认为这经常出现在编程中 . 需要注意的一点是,对于集合(列表,集等),主要问题是添加到集合 . 事情就是崩溃的地方 . 即使删除也行 .

    在大多数情况下,我们可以使用 Collection<? extends T> 而不是 Collection<T> ,这应该是第一选择 . 但是,我发现这样做并不容易 . 关于这是否总是最好的事情,我们有争议 . 我在这里提出一个类DownCastCollection,它可以将 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));
    }
    

    现在上课:

    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();
    
    }
    

    }

  • 0

    另一个解决方案是 Build 一个新的列表

    List<Dog> dogs = new ArrayList<Dog>(); 
    List<Animal> animals = new ArrayList<Animal>(dogs);
    animals.add(new Cat());
    
  • 7

    List<Dog> 不是 List<Animal> 的原因是,例如,你可以将 Cat 插入 List<Animal> ,但不能插入 List<Dog> ...你可以使用通配符使generics在可能的情况下更具可扩展性;例如,从 List<Dog> 读取类似于从 List<Animal> 读取 - 但不是写作 .

    Generics in the Java LanguageSection on Generics from the Java Tutorials有一个非常好的,深入的解释,为什么有些东西是多态的或者不是泛型的 .

  • 4

    我认为应该添加一点otheranswers提到就是这样

    List <Dog>不是Java中的List <Animal>

    这也是事实

    一份狗的名单 - 英文动物清单(嗯,在合理的解释下)

    OP的直觉起作用的方式 - 当然是完全有效的 - 是后一句 . 但是,如果我们应用这种直觉,我们会在其类型系统中获得一种非Java风格的语言:假设我们的语言允许将猫添加到我们的狗列表中 . 那是什么意思?这将意味着该名单不再是一个狗的名单,而只是一个动物名单 . 以及一系列哺乳动物和一系列四足动物 .

    换句话说:Java中的 List<Dog> 并不意味着"a list of dogs"的英文,它意味着"a list which can have dogs, and nothing else" .

    更一般地说, OP's intuition lends itself towards a language in which operations on objects can change their type ,或者更确切地说,对象的类型是其值的(动态)函数 .

  • 42

    让我们从JavaSE中获取示例tutorial

    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方法中,如果将传递圆形列表,形状的子类型,编译器应该检测到并限制编译错误到这样做) .

    出于显而易见的原因,这选择了第一种方式 .

  • 0

    实际上,您可以使用界面来实现您想要的效果 .

    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());
    
  • 0

    要理解这个问题,与数组进行比较是有用的 .

    List<Dog>not List<Animal> 的子类 .
    But Dog[] is Animal[] 的子类 .

    Arrays are reifiable and covariant .
    Reifiable表示其类型信息在运行时完全可用 .
    因此,数组提供运行时类型安全性但不提供编译时类型安全性 .

    // 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 are erased and invariant .
    因此泛型不能提供运行时类型安全性,但它们提供编译时类型安全性 .
    在下面的代码中,如果泛型是协变的,则可以在第3行进行heap pollution .

    List<Dog> dogs = new ArrayList<>();
        List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
        animals.add(new Cat());
    
  • 1

    这里给出的答案并没有完全说服我 . 相反,我做了另一个例子 .

    public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
        consumer.accept(supplier.get());
    }
    

    听起来不错,不是吗?但是你只能通过 ConsumerSupplier 来获取 Animal . 如果你有一个 Mammal 消费者,但是他们不应该适合,尽管两者都是动物 . 为了禁止这种情况,增加了额外的限制 .

    而不是上述,我们必须定义我们使用的类型之间的关系 .

    E. g . ,

    public <A extends Animal> void passOn(Consumer<A> consumer, Supplier<? extends A> supplier) {
        consumer.accept(supplier.get());
    }
    

    确保我们只能使用为消费者提供正确类型对象的供应商 .

    OTOH,我们也可以这样做

    public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<A> supplier) {
        consumer.accept(supplier.get());
    }
    

    我们走另一条路:我们定义 Supplier 的类型并限制它可以放入 Consumer .

    我们甚至可以做到

    public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<? extends A> supplier) {
        consumer.accept(supplier.get());
    }
    

    在哪里,有一个直观的关系 Life - > Animal - > Mammal - > DogCat 等,我们甚至可以将 Mammal 放入 Life 消费者,而不是 String 进入 Life 消费者 .

  • 32

    对于参数化类型,子类型是invariant . 即使类 DogAnimal 的子类型,参数化类型 List<Dog> 也不是 List<Animal> 的子类型 . 相反,数组使用covariant子类型,因此数组类型 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所说,这段代码是非法的,因为否则它会在狗预期时通过返回猫来违反类型限制 .

    将上述内容与数组的类似代码进行比较是有益的 .

    Dog[] dogs = new Dog[1];
    Object[] animals = dogs;
    animals[0] = new Cat(); // run-time error
    Dog dog = dogs[0];
    

    代码是合法的 . 但是,抛出一个array store exception . 数组在运行时携带其类型,这样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");
        }
    }
    

    使用命令 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
    }
    

    注意方法体的翻译代码是相同的 . 编译器用erasure替换了每个参数化类型 . 此属性至关重要,意味着它不会破坏向后兼容性 .

    总之,参数化类型无法实现运行时安全性,因为编译器会通过擦除替换每个参数化类型 . 这使得参数化类型只不过是语法糖 .

  • 3

    这种行为的基本逻辑是 Generics 遵循类型擦除机制 . 所以在运行时你无法确定 collection 的类型,而不像 arrays 那样没有这样的擦除过程 . 所以回到你的问题......

    所以假设有一种方法如下:

    add(List<Animal>){
        //You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism
    }
    

    现在,如果java允许调用者向此方法添加类型为Animal的List,然后您可能会将错误的东西添加到集合中,并且在运行时它也会因类型擦除而运行 . 在阵列的情况下,您将获得此类场景的运行时异常...

    因此,本质上实现了这种行为,以便人们不能将错误的东西添加到集合中 . 现在我相信类型擦除存在,以便与没有泛型的传统java兼容....

  • 1

    我会说泛型的全部意义在于它不允许这样做 . 考虑数组的情况,它允许这种类型的协方差:

    Object[] objects = new String[10];
      objects[0] = Boolean.FALSE;
    

    该代码编译良好,但抛出运行时错误(第二行中的 java.lang.ArrayStoreException: java.lang.Boolean ) . 它不是类型安全的 . 泛型的要点是添加编译时类型安全性,否则你可以坚持使用没有泛型的普通类 .

    现在有时你需要更灵活,这就是 ? super Class? extends Class 的用途 . 前者是需要插入类型 Collection (例如),后者是需要以类型安全的方式从中读取时 . 但同时做两者的唯一方法就是拥有一种特定的类型 .

相关问题