是列表 List的子类 ?为什么Java泛型不是隐式多态的?

问题描述 投票:696回答:16

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

假设以下层次结构 -

动物(父母)

狗 - 猫(儿童)

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

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

java generics inheritance polymorphism
16个回答
832
投票

不,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,但你不知道从它可以检索到什么,因为它可能是一个List<Object>


1
投票

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

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

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


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

}

}


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

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


0
投票

让我们以Java SE qazxsw poi为例

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) {
        ...
    }
}

所以Java“架构师”有2个选项来解决这个问题:

  1. 不要认为子类型是隐式的超类型,并给出编译错误,就像它现在发生的那样
  2. 考虑子类型是它的超类型并限制编译“添加”方法(所以在drawAll方法中,如果圆形列表,形状的子类型将被传递,编译器应检测到并限制编译错误那)。

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


0
投票

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

因此,我们有// drawAll method call drawAll(circleList); public void drawAll(List<Shape> shapes) { shapes.add(new Rectangle()); } ListOfAnimalListOfDog等,这些是我们指定泛型参数时最终由编译器“创建”的不同类。这是一个扁平的层次结构(实际上关于ListOfCat根本不是一个层次结构)。

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


0
投票

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

List

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


0
投票

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

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

72
投票

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

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


43
投票

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有一个非常好的,深入的解释,为什么有些东西是多态的或者不是泛型的。


32
投票

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

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

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

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


32
投票

我认为应该加入other answers所提到的一点是

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

这也是事实

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

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

换句话说:Java中的List<Dog>并不是指英语中的“狗列表”,它的意思是“可以有狗的列表,而不是其他”。

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


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

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
投票

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

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

听起来不错,不是吗?但你只能通过Consumers和Suppliers为Animals。如果你有一个Mammal消费者,但是Duck供应商,他们不应该适合,虽然两者都是动物。为了禁止这种情况,增加了额外的限制。

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

E.中,

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消费者。


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());
© www.soinside.com 2019 - 2024. All rights reserved.