list< dog>是list< animal>的一个子类吗?为什么java泛型不是隐式多态的?

m528fe3b  于 2021-07-11  发布在  Java
关注(0)|答案(13)|浏览(432)

我对java泛型如何处理继承/多态性有点困惑。
假设以下层次结构-
动物(父母)
狗-猫(儿童)
所以假设我有一个方法 doSomething(List<Animal> animals) . 根据继承和多态性的所有规则,我假设 List<Dog> 是一个 List<Animal> 和一个 List<Cat> 是一个 List<Animal> -所以任何一个都可以传递给这个方法。不是这样的。如果我想实现这个行为,我必须明确地告诉方法接受动物的任何子类的列表 doSomething(List<? extends Animal> animals) .
我知道这是java的行为。我的问题是为什么?为什么多态性通常是隐式的,但当涉及泛型时必须指定它?

xlpyo6sf

xlpyo6sf1#

对于参数化类型,子类型是不变的。甚至在班上都很难 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所说的,这个代码是非法的,因为否则它将违反类型约束,在狗需要时返回cat。
将上述代码与数组的类似代码进行比较是有指导意义的。

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
}

请注意,方法体的翻译代码是相同的。编译器将每个参数化类型替换为其擦除。这个属性至关重要,这意味着它没有破坏向后兼容性。
总之,参数化类型的运行时安全性是不可能的,因为编译器用其擦除来替换每个参数化类型。这使得参数化类型只不过是语法糖。

g6baxovj

g6baxovj2#

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

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

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

jdg4fx2g

jdg4fx2g3#

答案和其他答案都是正确的。我将用一个我认为有用的解决方案来补充这些答案。我认为这在编程中经常出现。需要注意的一点是,对于集合(列表、集合等),主要问题是添加到集合中。这就是事情发生的地方。即使移除也可以。
在大多数情况下,我们可以使用 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();

}

}

oyjwcjzk

oyjwcjzk4#

让我们以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“架构师”有两个选项来解决这个问题:
不要认为子类型隐式地是它的父类型,并给出一个编译错误,就像现在发生的那样
将子类型视为它的超类型a

pjngdqdw

pjngdqdw5#

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

突然你有一只非常困惑的猫。
现在,你不能添加 CatList<? extends Animal> 因为你不知道这是一个 List<Cat> . 您可以检索一个值并知道它将是一个 Animal ,但不能添加任意的动物。反之亦然 List<? super Animal> -在这种情况下,您可以添加 Animal 但你不知道从中可以得到什么,因为它可能是一个 List<Object> .

7gs2gvoe

7gs2gvoe6#

你要找的是所谓的协变类型参数。这意味着如果一种类型的对象可以在方法中替换为另一种类型的对象(例如, Animal 可以替换为 Dog ),这同样适用于使用这些对象的表达式(因此 List<Animal> 可以替换为 List<Dog> ). 问题是协方差通常对于可变列表是不安全的。假设你有一个 List<Dog> ,它被用作 List<Animal> . 如果你想把猫加到这里面怎么办 List<Animal> 这真是一个 List<Dog> ? 自动允许类型参数协变会破坏类型系统。
添加语法以允许将类型参数指定为协变参数是很有用的,这样可以避免 ? extends Foo 但这确实增加了额外的复杂性。

xtfmy6hx

xtfmy6hx7#

原因是 List<Dog> 不是一个 List<Animal> ,例如,可以插入 Cat 变成一个 List<Animal> ,但不会变成 List<Dog> ... 您可以使用通配符使泛型在可能的情况下更具可扩展性;例如,从一个 List<Dog> 类似于从一个 List<Animal> --但不是写作。
java语言中的泛型和java教程中关于泛型的部分都有一个非常好的、深入的解释,解释了为什么有些东西是多态的或不多态的,或者泛型允许的。

yws3nbqq

yws3nbqq8#

我认为应该补充一点,其他答案提到的是 List<Dog> 不是吗 List<Animal> 在java中
这也是事实
狗的名单是英文的动物名单(根据合理的解释)
op直觉的运作方式——这当然是完全有效的——是后一句话。然而,如果我们应用这种直觉,我们会得到一种在类型系统中不是java风格的语言:假设我们的语言允许在狗的列表中添加一只猫。那是什么意思?这意味着名单不再是狗的名单,而仅仅是动物的名单。一份哺乳动物的名单,一份四足动物的名单。
换一种说法:a List<Dog> 在java中,在英语中并不是“狗的列表”的意思,它的意思是“可以有狗的列表,没有别的”。
更一般地说,op的直觉倾向于一种语言,在这种语言中,对对象的操作可以改变它们的类型,或者更确切地说,对象的类型是其值的(动态)函数。

1yjd4xko

1yjd4xko9#

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

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

这段代码编译得很好,但会引发运行时错误( java.lang.ArrayStoreException: java.lang.Boolean 在第二行)。这是不安全的。泛型的要点是添加编译时类型安全性,否则您只能使用没有泛型的普通类。
现在有些时候你需要更加灵活,这就是 ? super Class 以及 ? extends Class 是给你的。前者是需要插入到类型中的时候 Collection (例如),后者用于需要以类型安全的方式从中读取的情况。但同时做这两件事的唯一方法是有一个特定的类型。

egdjgwm8

egdjgwm810#

为了理解这个问题,比较数组是很有用的。 List<Dog> 不是的子类 List<Animal> .
但是 Dog[] 是的子类 Animal[] .
数组是可重定和协变的。
refiable意味着它们的类型信息在运行时完全可用。
因此,数组提供运行时类型安全,但不提供编译时类型安全。

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

o0lyfsai11#

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

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

听起来不错,不是吗?但你只能通过 Consumer s和 Supplier s代表 Animal s。如果你有 Mammal 消费者,但是 Duck 供应商,他们不应该适合虽然都是动物。为了不允许这样做,增加了额外的限制。
我们必须定义所使用类型之间的关系,而不是上述内容。
e。g、 哦。,

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

确保我们只能使用为消费者提供正确对象类型的供应商。
奥托,我们也可以

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 -> Dog , Cat 等等,我们甚至可以 Mammal 变成一个 Life 消费者,但不是 String 变成一个 Life 消费者。

bfnvny8b

bfnvny8b12#

这种行为的基本逻辑是 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的列表添加到这个方法中,那么您可能会将错误的东西添加到集合中,并且在运行时它也会由于类型擦除而运行。而在数组的情况下,对于这样的场景,您将得到一个运行时异常。。。
因此在本质上,这种行为的实现是为了不向集合中添加错误的东西。现在我相信类型擦除的存在是为了在没有泛型的情况下提供与遗留java的兼容性。。。。

wa7juj8i

wa7juj8i13#

实际上,你可以使用一个接口来实现你想要的。

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

相关问题