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

假设以下层次结构-

动物(父母)

狗-猫(儿童)

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

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


不,列表<狗>不是列表<动物>。考虑一下你可以用列表<动物>做什么——你可以在其中添加任何动物……包括一只猫。现在,你能在一窝小狗中加入一只猫吗?绝对不是。

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

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

现在,您不能将猫添加到列表<?扩展Animal>,因为你不知道它是List<Cat>。您可以检索一个值并知道它将是一个Animal,但不能添加任意的Animal。列表<?super Animal>-在这种情况下,您可以安全地将Animal添加到其中,但您不知道可能从中检索到什么,因为它可能是List<Object>。


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

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


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

《Java语言中的泛型》和《Java教程》中的“泛型”一节对为什么某些事物是多态的或不多态的或允许使用泛型进行了非常好、深入的解释。


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

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

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

现在有些时候你需要更加灵活,这就是为什么?超级班和?扩展类用于。前者是当您需要插入到类型Collection中时(例如),后者是当需要以类型安全的方式从中读取时。但同时做到这两个的唯一方法是拥有一种特定的类型。


这种行为的基本逻辑是泛型遵循类型擦除机制。因此,在运行时,您无法识别集合的类型,而不像阵列中没有这样的擦除过程。回到你的问题。。。

因此,假设有如下方法:

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

现在,若java允许调用者将List of type Animal添加到此方法中,那个么您可能会将错误的内容添加到集合中,并且在运行时它也会由于类型擦除而运行。而在数组的情况下,您将获得此类场景的运行时异常。。。

因此,本质上,这种行为的实现是为了避免将错误的东西添加到集合中。现在我相信类型删除的存在是为了与没有泛型的遗留java兼容。。。。


我认为,其他答案中提到的一点是

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

这也是事实

狗的列表是英文的动物列表(在合理的解释下)

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

换一种说法:Java中的List<Dog>在英语中并不意味着“一个狗的列表”,它的意思是“一个只有狗的列表。”。

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


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

在大多数情况下,我们可以使用集合<?扩展T>而不是集合<T>,这应该是首选。然而,我发现这样做并不容易。关于这是否始终是最好的做法,这是一个值得争论的问题。我在这里介绍一个类DownCastCollection,它可以转换集合<?将T>扩展到集合<T>(我们可以为List、Set、NavigableSet等定义类似的类),以便在使用标准方法时使用。下面是一个如何使用它的示例(在这种情况下,我们也可以使用Collection<?extendsObject>,但我保持简单,以说明使用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();

}

}


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

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

听起来不错,不是吗?但你只能通过动物消费者和供应商。如果你有哺乳动物的消费者,但鸭子的供应商,尽管它们都是动物,但它们不应该适合。为了禁止这种行为,增加了额外的限制。

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

例如。,

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

我们的方向相反:我们定义供应商的类型,并限制其可以投放给消费者。

我们甚至可以做到

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

其中,有了“生活”->“动物”->“哺乳动物”->“狗”、“猫”等直观关系,我们甚至可以将“哺乳动物”放入“生活”消费者,但不能将“字符串”放入“生命”消费者。


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

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

当您想要在构造函数内部传递列表或对其进行迭代时,这是非常有用的。


让我们以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“架构师”有两个选项可以解决这个问题:

不要认为子类型是隐式的,它是父类型,并给出编译错误,就像现在发生的那样将子类型视为它的父类型,并在编译“add”方法时进行限制(因此在drawAll方法中,如果要传递圆的列表(形状的子类型),编译器应该检测到这一点,并用编译错误限制您这样做)。

出于明显的原因,他们选择了第一条路。


为了理解这个问题,比较数组是很有用的。

List<Dog>不是List<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());

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

因此,我们有ListOfAnimal、ListOfDog、ListOfCat等,它们是不同的类,当我们指定泛型参数时,最终由编译器“创建”。这是一个扁平的层次结构(实际上关于List根本不是一个层次结构)。

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


问题已经很好地确定了。但有一个解决方案;使doSomething通用:

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

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


子类型对于参数化类型是不变的。即使严格来说,类Dog是Animal的子类型,但参数化类型List<Dog>不是List<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演示,这将显示以下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
}

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

总之,参数化类型的运行时安全性是不可能的,因为编译器通过删除来替换每个参数化类型。这使得参数化类型只不过是语法糖。


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

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

关于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?

在最深层次上,这里的问题是狗和动物共享一个参考。这意味着一种方法是复制整个列表,这将打破引用相等:

// 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<>(狗);后;,您随后不能将动物直接分配给狗或猫:

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

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

显然,这改变了语义,因为动物和狗的列表不再共享,所以添加到一个列表不会添加到另一个列表中(这正是您想要的,以避免将猫添加到只应包含狗对象的列表中的问题)。此外,复制整个列表可能效率低下。然而,通过打破引用相等性,这确实解决了类型等价问题。


问题已正确识别为与差异有关,但详细信息不正确。纯函数列表是协变数据函子,这意味着如果Sub类型是Super的子类型,那么Sub列表绝对是Super列表的子类型。

然而,列表的可变性并不是这里的基本问题。问题是总体上的可变性。这个问题是众所周知的,被称为协方差问题,我认为它是卡斯塔尼亚首先发现的,它完全彻底地破坏了作为一个通用范式的对象定向。这是基于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受到质疑时,为什么要使用面向对象的垃圾呢??

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

https://en.wikipedia.org/wiki/Covariance_and_contravariance_(计算机科学)


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

让我们继续创建一个简化的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()方法被定义为接受Animal类型的任何东西,它可以接受Animal类型的数组,也可以接受Dog类型的数组。这就是多态性的作用。

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

现在假设我们稍微调整一下代码,使用ArrayList而不是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-Animal类型的任何ArrayList,因此它不应该成为交易破坏者。

但是,不幸的是,编译器抛出了一个错误,不允许我们将Dog ArrayList传递给期望Animal ArrayList的变量。

你问为什么?

因为想象一下,如果JAVA允许将Dog ArrayList-dogs-放入Animal ArrayList中-animals-然后在takeAnimals()方法中,有人会这样做-

animals.add(new Cat());

认为这应该是可行的,因为理想情况下它是一个Animal ArrayList,您应该能够将任何猫添加到其中,作为cat-is-also-Animal,但实际上您将一个Dog类型的ArrayList传递给了它。

所以,现在您必须想到,数组也应该发生同样的情况。你这样想是对的。

如果有人试图用Arrays做同样的事情,那么Arrays也会抛出一个错误,但Arrays在运行时处理这个错误,而ArrayList在编译时处理这个问题。


其他人已经很好地解释了为什么不能将后代列表转换为超类列表。

然而,许多人访问这个问题以寻求解决方案。

因此,自Java版本10以来,该问题的解决方案如下:

(注:S=超类)

List<S> supers = List.copyOf( descendants );

如果完全安全,则此函数将执行强制转换,如果强制转换不安全,则该函数将执行复制。

有关深入解释(考虑到此处其他答案提到的潜在陷阱),请参阅相关问题和我2022年的答案:https://stackoverflow.com/a/72195980/773113