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


769

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

假设以下层次结构-

动物(父母)

- (儿童)

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

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


17
一个完全无关的语法问题现在困扰着我-我的标题应该是“为什么不是 Java的泛型”或“为什么不是 Java的泛型”?是“泛型”是因为s是复数还是因为它是一个实体而是单数?
froadie 2010年

25
Java中的泛型是一种非常差的参数多态形式。别像我以前那样对他们过分相信,因为有一天您会受到他们可怜的局限性的困扰外科医生扩展了Handable <Scalpel>,Handable <Sponge> KABOOM!难道计算[TM]。有Java泛型限制。任何OOA / OOD都可以很好地翻译成Java(MI可以使用Java接口很好地完成),但是泛型并不能削减它。它们非常适合所说的“集合”和过程编程(无论如何,大多数Java程序员都是这样做的……)。
语法T3rr0r,2010年

7
List <Dog>的超类不是List <Animal>,而是List <?>(即未知类型的列表)。泛型会删除已编译代码中的类型信息。这样做是为了使使用泛型(Java 5及更高版本)的代码与没有泛型的Java早期版本兼容。
rai.skumar,2012年


9
@froadie,因为似乎没有人响应...绝对应该是“为什么不是Java的泛型...”。另一个问题是“泛型”实际上是一个形容词,因此“泛型”是指被“泛型”修饰的删除的复数名词。您可以说“该函数是通用的”,但这比说“该函数是通用的”更麻烦。但是,说“ Java具有泛型函数和类”而不是仅仅“ Java具有泛型”是有点麻烦的。作为写关于形容词的硕士论文的人,我认为您偶然发现了一个非常有趣的问题!
丹迪斯顿'17

Answers:


915

不,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?

突然,你有一只非常困惑的猫。

现在,您无法将a添加Cat到a,List<? extends Animal>因为您不知道它是一个List<Cat>。您可以检索一个值并知道它将是一个值Animal,但是您不能添加任意动物。相反的情况是正确的List<? super Animal>-在这种情况下,您可以Animal安全地向其中添加一个,但是对于从中检索到的内容一无所知,因为它可能是个List<Object>


49
有趣的,就像直觉告诉我们的那样,每条狗的确是动物的清单。关键是,不是每一个动物列表都是狗列表,因此通过添加猫来改变列表是一个问题。
Ingo 2013年

66
@Ingo:不,不是真的:您可以将猫添加到动物列表中,但是不能将猫添加到狗列表中。如果您将其视为只读,则狗列表仅是动物列表。
乔恩·斯基特

12
@JonSkeet-当然,但是谁在强制要求从猫和狗的清单中重新列出清单却实际上改变了狗的清单?这是Java中的任意实现决策。与逻辑和直觉相反的一种。
Ingo

7
@Ingo:首先我不会使用它。如果您有一个列表在顶部显示“我们可能想去的酒店”,然后有人在其中添加了游泳池,您认为这有效吗?不-这是酒店列表,而不是建筑物列表。而且,这甚至不像我什至说过“狗名单不是动物名单”-我用代号,用代号表示。我真的不认为这里有任何歧义。无论如何,使用子类都是不正确的-它与分配兼容性有关,而不是与子类有关。
乔恩·斯基特

14
@ruakh:问题在于您要在执行时加倍执行,而这在编译时可能会阻塞。我认为数组协方差是一个设计错误。
乔恩·斯基特

83

您正在寻找的被称为协变类型参数。这意味着,如果一种类型的对象可以在方法中替代另一种类型(例如,Animal可以用替换Dog),则使用这些对象的表达式也是如此(因此List<Animal>可以用替换List<Dog>)。问题在于,协方差通常对于可变列表并不安全。假设您有个List<Dog>,并且正在将其用作List<Animal>。当您尝试向其中添加Cat List<Animal>(实际上是Cat)时会发生什么List<Dog>?自动允许类型参数协变会破坏类型系统。

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


44

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

Java语言的泛型从Java教程仿制药科有一个很好的,深入的解释,为什么有些东西是或不是多晶型或泛型允许的。


36

我认为应该在其他 答案中提到的一点是

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

的确,

狗的清单是英语的动物清单(在合理的解释下)

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

换句话说List<Dog>,Java中的A 并不意味着英语中的“狗列表”,而是“可以有狗的列表,别无其他”。

更一般而言,OP的直觉倾向于一种语言,在该语言上,对对象的操作可以更改其类型,或者更确切地说,对象的类型是其值的(动态)函数。


是的,人类的语言更加模糊。但是,一旦将其他动物添加到狗列表中,它仍然是动物列表,但不再是狗列表。区别在于,具有模糊逻辑的人通常没有意识到这一点的问题。
Vlasec

当有人发现数组的常数比较更加令人困惑时,这个答案对我很重要。我的问题是语言的直觉。
19

32

我想说泛型的全部要点是它不允许这样做。考虑一下数组的情况,它确实允许这种类型的协方差:

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

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

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


13
可以说,数组协方差是一种语言设计错误。请注意,由于类型擦除,泛型集合在技术上不可能实现相同的行为。
Michael Borgwardt

我要说泛型的全部意思是它不允许这样做。 ” 您永远不能确定:Java和Scala的类型系统不健全:空指针的存在危机(在OOPSLA 2016上展示)(似乎已得到纠正)
David Tonhofer

15

要了解该问题,与数组进行比较很有用。

List<Dog>不是子类List<Animal>
但是 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<Dog> dogs = new ArrayList<>();
    List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
    animals.add(new Cat());

2
可能正因为如此,Java的数组坏了,可能会引起争议,
leonbloy

协变数组是编译器的“功能”。
Cristik '18

6

这里给出的答案并不能完全说服我。因此,我再举一个例子。

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

听起来不错,不是吗?但是您只能将Consumers和Suppliers 传递给Animals。如果您有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- > Animal- > Mammal- > DogCat等等,我们甚至可以在一MammalLife消费者,而不是StringLife消费者。


1
在4个版本中,#2可能是错误的。例如,我们不能用(Consumer<Runnable>, Supplier<Dog>)while Dog来称呼它,它是Animal & Runnable
ZhongYu 2015年

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兼容。


4

子类型对于参数化类型是不变的。即使强壮的类Dog是的子类型Animal,参数化的类型List<Dog>也不是的子类型List<Animal>。相反,数组使用协变子类型,因此数组类型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];

该代码是合法的。但是,抛出一个数组存储异常。数组以这种方式在运行时携带其类型,从而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
}

请注意,方法主体的翻译代码相同。编译器通过擦除来替换每个参数化类型。此属性至关重要,意味着它不会破坏向后兼容性。

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


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

1

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

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

当您要在构造函数中传递列表或对其进行迭代时,这很有用


2
这将带来比实际解决的问题更多的问题
Ferrybig '16

如果您尝试将猫添加到列表中,请确保它会产生问题,但是出于循环目的,我认为它是唯一的非冗长答案。
sagits's

1

答案 以及其他的答案是正确的。我将使用我认为会有所帮助的解决方案来补充这些答案。我认为这经常在编程中出现。需要注意的一件事是,对于集合(列表,集合等),主要问题是添加到集合中。那是事情崩溃的地方。即使删除也可以。

在大多数情况下,我们可以使用Collection<? extends T>而不是Collection<T>,那应该是首选。但是,我发现不容易做到这一点的情况。关于这是否始终是最好的事情,有待辩论。我在这里展示的是DownCastCollection类,该类可以将convert转换Collection<? extends T>为a 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();

}

}


这是一个好主意,以至于它已经存在于Java SE中。; )Collections.unmodifiableCollection
Radiodef

1
是的,但是我定义的集合可以修改。
丹b 2015年

是的,可以修改。Collection<? extends E>但是,除非您以非类型安全的方式使用该行为(例如,将其强制转换为其他内容),否则已经正确处理了该行为。我看到的唯一好处是,当您调用该add操作时,即使您强制转换它也会引发异常。
Vlasec

0

让我们以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“建筑师”有2个选项可以解决此问题:

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

  2. 认为该子类型是其父类型,并限制在编译“ add”方法时使用(因此在drawAll方法中,如果将传递一系列圆形,形状的子类型,则编译器应检测到该情况,并限制您执行编译错误那)。

由于明显的原因,选择了第一种方法。


0

我们还应该考虑编译器如何威胁通用类:在填充通用参数时,“实例化”另一种类型。

因此,我们有ListOfAnimalListOfDogListOfCat,等,这是不同的类,最终被通过时,我们指定了通用参数的编译器“创造”。这是一个平坦的层次结构(实际上,关于List根本不是层次结构)。

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


0

该问题已得到充分认识。但是有一个解决方案。使doSomething通用:

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

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


0

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

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

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);,您无法随后直接分配animalsdogscats

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

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.