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

时间:2010-04-30 14:39:39

标签: java generics inheritance polymorphism

我对Java泛型如何处理继承/多态性感到有点困惑。

假设以下层次结构 -

动物(家长)

- (儿童)

假设我有一个方法doSomething(List<Animal> animals)。根据所有继承和多态的规则,我假设List<Dog> List<Animal>List<Cat> a { {1}} - 所以任何一个都可以传递给这个方法。不是这样。如果我想实现这种行为,我必须通过说List<Animal>显式地告诉该方法接受Animal的任何子类的列表。

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

19 个答案:

答案 0 :(得分:838)

不,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<Cat>。您可以检索一个值并知道它将是Animal,但您无法添加任意动物。对于List<? super Animal>,反之亦然 - 在这种情况下,您可以安全地向其添加Animal,但您不知道从中检索到什么,因为它可能是{{1} }}

答案 1 :(得分:73)

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

添加语法以允许将类型参数指定为协变会很有用,这样可以避免方法声明中的List<Dog>,但这会增加额外的复杂性。

答案 2 :(得分:43)

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

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

答案 3 :(得分:32)

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

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

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

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

答案 4 :(得分:32)

我认为应该在other answers提到的内容中添加一点,而

  

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

也是如此。

  

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

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

换句话说:Java中的List<Dog>并不意味着&#34;狗的列表&#34;在英语中,它表示&#34;一个可以有狗的列表,没有别的&#34;。

更一般地说, OP的直觉适用于一种语言,在该语言中,对象的操作可以改变其类型,或者更确切地说,对象的类型是(动态)其价值的功能。

答案 5 :(得分:8)

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

List<Dog>不是 List<Animal>的子类 Dog[] Animal[]的子类。

数组是reifiable和协变
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

对于泛型,反之亦然:
泛型为erased且不变
因此泛型不能提供运行时类型安全性,但它们提供编译时类型安全性。
在下面的代码中,如果泛型是协变的,则可以在第3行进行heap pollution

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

答案 6 :(得分:5)

此类行为的基本逻辑是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兼容....

答案 7 :(得分:4)

这里给出的答案并没有完全说服我。所以,我做了另一个例子。

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

听起来不错,不是吗?但是,您只能为Consumer传递SupplierAnimal s。如果您有一个Mammal消费者,但Duck供应商,他们不应该适合,尽管两者都是动物。为了禁止这种情况,增加了额外的限制。

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

电子。克,

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 - &gt; Animal - &gt; Mammal - &gt; DogCat等,我们甚至可以将Mammal加入Life个消费者,而不是String加入Life个消费者。

答案 8 :(得分:3)

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

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

答案 9 :(得分:1)

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

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

当你想在构造函数中传递列表或迭代它时,这很有用

答案 10 :(得分:1)

参数化类型的子类型为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替换了每个参数化类型。此属性至关重要,意味着它不会破坏向后兼容性。

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

答案 11 :(得分:1)

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

}

}

答案 12 :(得分:0)

让我们从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&#34;架构师&#34;有2个选项可以解决这个问题:

  1. 不要认为子类型是隐含的超类型,并且给出编译错误,就像它现在发生的那样

  2. 将子类型视为它的超类型,并在编译时限制&#34;添加&#34;方法(因此在drawAll方法中,如果将传递圆形列表,形状的子类型,编译器应该检测到并限制编译错误)。

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

答案 13 :(得分:0)

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

因此,我们有<!----- HTML -----> <div class="title linklike"> <span class="plus">&#9656;</span> <span class="minus">&#9662;</span> <a class="linklike" onclick="toggle_att_menu(this)" href="#">condition</a> </div> <div class="search-attribute hide-list" data-attr="condition"> <ul class="att-list"> <li class="checkbox"> <label> <input name="condition" class="multi_checkbox" value="1" type="checkbox"/> new </label> </li> <li class="checkbox"> <label> <input name="condition" class="multi_checkbox" value="2" type="checkbox"/> like new </label> </li> <li class="checkbox"> <label> <input name="condition" class="multi_checkbox" value="3" type="checkbox"/> excellent </label> </li> <li class="checkbox"> <label> <input name="condition" class="multi_checkbox" value="4" type="checkbox"/> good </label> </li> <li class="checkbox"> <label> <input name="condition" class="multi_checkbox" value="5" type="checkbox"/> fair </label> </li> <li class="checkbox"> <label> <input name="condition" class="multi_checkbox" value="6" type="checkbox"/> damaged </label> </li> </ul> </div> <div class="title linklike"> <span class="plus">&#9656;</span> <span class="minus">&#9662;</span> <a class="linklike" onclick="toggle_att_menu(this)" href="#">condition</a> </div> <div class="search-attribute hide-list" data-attr="condition"> <ul class="att-list"> <li class="checkbox"> <label> <input name="condition" class="multi_checkbox" value="1" type="checkbox"/> new </label> </li> <li class="checkbox"> <label> <input name="condition" class="multi_checkbox" value="2" type="checkbox"/> like new </label> </li> <li class="checkbox"> <label> <input name="condition" class="multi_checkbox" value="3" type="checkbox"/> excellent </label> </li> <li class="checkbox"> <label> <input name="condition" class="multi_checkbox" value="4" type="checkbox"/> good </label> </li> <li class="checkbox"> <label> <input name="condition" class="multi_checkbox" value="5" type="checkbox"/> fair </label> </li> <li class="checkbox"> <label> <input name="condition" class="multi_checkbox" value="6" type="checkbox"/> damaged </label> </li> </ul> </div>ListOfAnimalListOfDog等,它们是不同的类,当我们指定泛型参数时,最终由编译器“创建”。这是一个扁平的层次结构(实际上关于ListOfCat根本不是一个层次结构。)

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

答案 14 :(得分:0)

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

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

现在您可以使用List&lt; Dog&gt;调用doSomething或列表&lt; Cat&gt;或列出&lt; Animal&gt;。

答案 15 :(得分:0)

另一种解决方案是建立新列表

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

答案 16 :(得分:0)

Jon Skeet进一步回答,该示例代码如下:

// 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?

从最深层次看,这里的问题是dogsanimals共享一个引用。这意味着完成这项工作的一种方法是复制整个列表,这将破坏引用的相等性:

// This code is fine
List<Dog> dogs = new ArrayList<Dog>();
dogs.add(new Dog());
List<Animal> 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<Animal> animals = new ArrayList<>(dogs);之后,您不能随后将animals直接分配给dogscats

// These are both illegal
dogs = animals;
cats = animals;

因此,您不能将错误的Animal子类型放入列表中,因为没有错误的子类型-子类型? extends Animal的任何对象都可以添加到animals中。 / p>

很显然,这改变了语义,因为列表animalsdogs不再共享,因此添加到一个列表不会添加到另一个列表(这正是您想要的,以避免Cat可以添加到仅应包含Dog对象的列表中的问题)。同样,复制整个列表可能效率很低。但是,这确实可以通过打破引用相等来解决类型对等问题。

答案 17 :(得分:0)

该问题已被正确识别为与差异有关,但细节不正确。纯函数列表是协变量数据函子,这意味着,如果Sub类型是Super的子类型,那么Sub的列表肯定是Super列表的子类型。

但是,列表的可变性不是这里的基本问题。问题通常是可变性。这个问题是众所周知的,被称为协方差问题,我认为是Castagna首次发现的,它完全彻底地破坏了面向对象的范式。它基于Cardelli和Reynolds先前建立的方差规则。

有些简化,让我们考虑将类型T的对象B分配给类型T的对象A作为突变。这不会失去一般性:可以将A的突变写为A = f(A),其中f:T->T。当然,问题在于,尽管函数在其共域中是协变的,但它们在函数中却是协变的域,但是分配时域和共域是相同的,因此分配是不变的!

由此得出结论,子类型不能被突变。但是,由于面向对象的突变是根本的,因此面向对象在本质上是有缺陷的。

这是一个简单的示例:在纯函数设置中,对称矩阵显然是一个矩阵,它是子类型,没问题。现在,让矩阵添加将单个元素设置为坐标(x,y)的功能,并且规则没有其他元素更改。现在,对称矩阵不再是子类型,如果更改(x,y),则也更改了(y,x)。功能运算是delta:Sym-> Mat,如果更改对称矩阵的一个元素,则会得到一般的非对称矩阵。因此,如果您在Mat中包括了“更改一个元素”方法,则Sym不是子类型。实际上..几乎肯定没有适当的亚型。

简而言之:如果您的通用数据类型具有多种利用其通用性的变异子,则可以确定任何适当的子类型都不可能支持所有这些变异:如果可以,那将仅仅是与父类型一样通用,与“适当”子类型的规范相反。

Java防止对可变列表进行子类型化这一事实未能解决一个真正的问题:为什么几十年前被Java抹黑时,为什么要使用像Java这样的面向对象的垃圾?

无论如何,这里都有一个合理的讨论:

https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)

答案 18 :(得分:0)

我看到这个问题已经被回答了很多次,只是想在同一问题上输入我的意见。

让我们继续创建简化的Animal类层次结构。

abstract class Animal {
    void eat() {
        System.out.println("animal eating");
    }
}

class Dog extends Animal {
    void bark() { }
}

class Cat extends Animal {
    void meow() { }
}

现在让我们看看我们的老朋友Arrays,我们知道它隐式支持多态-

class TestAnimals {
    public static void main(String[] args) {
        Animal[] animals = {new Dog(), new Cat(), new Dog()};
        Dog[] dogs = {new Dog(), new Dog(), new Dog()};
        takeAnimals(animals);
        takeAnimals(dogs);
    }

    public void takeAnimals(Animal[] animals) {
        for(Animal a : animals) {
            System.out.println(a.eat());
        }
    }   
}

该类编译良好,当我们运行上述类时,我们得到了输出

animal eating
animal eating
animal eating
animal eating
animal eating
animal eating

这里要注意的一点是,将takeAnimals()方法定义为采用动物类型的任何东西,可以采用动物类型的数组,也可以采用Dog的数组,因为Dog-is-a -动物。这就是行动中的多态。

现在让我们对泛型使用相同的方法,

现在说我们稍微调整一下代码,并使用ArrayLists代替Arrays-

class TestAnimals {
    public static void main(String[] args) {
        ArrayList<Animal> animals = new ArrayList<Animal>();
        animals.add(new Dog());
        animals.add(new Cat());
        animals.add(new Dog());
        takeAnimals(animals);
    }

    public void takeAnimals(ArrayList<Animal> animals) {
        for(Animal a : animals) {
            System.out.println(a.eat());
        }
    }   
}

上面的类将编译并产生输出-

animal eating
animal eating
animal eating
animal eating
animal eating
animal eating

所以我们知道这是可行的,现在让我们稍微调整一下该类以多态使用Animal类型-

class TestAnimals {
    public static void main(String[] args) {
        ArrayList<Animal> animals = new ArrayList<Animal>();
        animals.add(new Dog());
        animals.add(new Cat());
        animals.add(new Dog());

        ArrayList<Dog> dogs = new ArrayList<Dog>();
        takeAnimals(animals);
        takeAnimals(dogs);
    }

    public void takeAnimals(ArrayList<Animal> animals) {
        for(Animal a : animals) {
            System.out.println(a.eat());
        }
    }   
}

看起来像编译上面的类应该没有问题,因为takeAnimals()方法被设计为采用任何Animal和Dog-is-a-Animal类型的ArrayList,所以这里不应该成为问题的解决方法。

但是,不幸的是,编译器会引发错误,并且不允许我们将Dog ArrayList传递给期望Animal ArrayList的变量。

您问为什么?

因为可以想象,如果JAVA允许将Dog ArrayList(狗)放入Animal ArrayList(动物)中,然后在takeAnimals()方法内部,则有人会执行类似的操作

animals.add(new Cat());

认为这应该可行,因为理想情况下它是Animal ArrayList,并且您应该可以将任何猫添加为Cat-is-also-a-Animal,但实际上您将Dog类型的ArrayList传递给了它。

因此,现在您必须考虑数组也应该发生相同的情况。您的想法是对的。

如果有人尝试对Arrays做同样的事情,那么Arrays也将引发错误,但是Arrays在运行时会处理此错误,而ArrayLists在编译时会处理此错误。