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

假设以下层次结构-

动物(父母)

狗-猫(儿童)

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

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


当前回答

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

因此,假设有如下方法:

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

其他回答

让我们以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<Animal>) (List<?>) dogs

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

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

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

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

(注:S=超类)

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

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

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

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

在大多数情况下,我们可以使用集合<?扩展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());
}

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