List是List的一个子类吗?为什么Java泛型不是隐式多态的?

List是List的一个子类吗?为什么Java泛型不是隐式多态的?,java,generics,inheritance,polymorphism,Java,Generics,Inheritance,Polymorphism,我对Java泛型如何处理继承/多态性有点困惑 假设以下层次结构- 动物亲本 狗猫儿童 因此,假设我有一个方法来对付动物。根据继承和多态性的所有规则,我假设一个列表就是一个列表,一个列表就是一个列表,所以任何一个都可以传递给这个方法。不是这样。如果我想实现这种行为,我必须明确告诉方法接受动物任何子类的列表,方法是说doSomethingListNo,列表不是列表。考虑一下你可以用一个列表来做什么,你可以添加任何动物。包括一只猫。现在,你能在一窝小狗中添加一只猫吗?绝对不是 // Illegal c

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

假设以下层次结构-

动物亲本

狗猫儿童

因此,假设我有一个方法来对付动物。根据继承和多态性的所有规则,我假设一个列表就是一个列表,一个列表就是一个列表,所以任何一个都可以传递给这个方法。不是这样。如果我想实现这种行为,我必须明确告诉方法接受动物任何子类的列表,方法是说doSomethingListNo,列表不是列表。考虑一下你可以用一个列表来做什么,你可以添加任何动物。包括一只猫。现在,你能在一窝小狗中添加一只猫吗?绝对不是

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


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

列表不是列表的原因,对于为什么有些东西是多态的或不多态的,或者泛型允许的,有一个非常好的、深入的解释。

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

  Object[] objects = new String[10];
  objects[0] = Boolean.FALSE;
该代码编译良好,但在第二行抛出运行时错误java.lang.ArrayStoreException:java.lang.Boolean。这是不安全的。泛型的要点是添加编译时类型安全性,否则您只能使用没有泛型的普通类


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

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

因此,假设有一种方法,如下所示:

add(List<Animal>){
    //You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism
}
现在,若java允许调用方将类型Animal的列表添加到此方法中,那个么您可能会将错误的内容添加到集合中,并且在运行时,由于类型擦除,它也将运行。而对于阵列,您将获得此类场景的运行时异常


因此,本质上,这种行为的实现是为了防止向集合中添加错误的内容。现在我相信类型擦除的存在是为了在不使用泛型的情况下与遗留java保持兼容性……

我认为应该补充一点

列表不是Java中的列表

这也是事实

《狗的名单》——在合理解释下的英语动物名单

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

换句话说:Java中的列表在英语中并不意味着狗的列表,它意味着可以有狗的列表,而不是别的

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

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


在大多数情况下,我们可以使用Collection这里给出的答案并不能完全说服我。因此,我举了另一个例子

public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
    consumer.accept(supplier.get());
}
确保我们只能使用为消费者提供正确对象类型的供应商

奥托,我们也可以这么做

public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<A> supplier) {
    consumer.accept(supplier.get());
}
其中,具有直觉关系Lif
e->动物->哺乳动物->狗、猫等,我们甚至可以将哺乳动物放入生活消费品中,但不能将字符串放入生活消费品中。

实际上,您可以使用界面来实现您想要的

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

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

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

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

让我们以JavaSE为例

因此,为什么狗圈列表不应被视为隐含的动物形状列表,是因为这种情况:

// drawAll method call
drawAll(circleList);


public void drawAll(List<Shape> shapes) {
   shapes.add(new Rectangle());    
}
因此Java架构师有两个选项来解决这个问题:

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

将子类型视为它的超类型,并在编译add方法时进行限制,因此在drawAll方法中,如果要传递一个圆列表(shape的子类型),编译器应该检测到这一点,并使用编译错误限制您这样做


出于显而易见的原因,他们选择了第一条道路

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

列表不是列表和协变的子类。表示其类型信息在运行时完全可用。因此,数组提供运行时类型安全性,但不提供编译时类型安全性

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

我们还应该考虑编译器是如何威胁泛型类的:无论何时填充泛型参数,它都会实例化不同的类型

因此,我们有listofaimal、listofalog、listofacat等,它们是不同的类,当我们指定泛型参数时,编译器最终会创建这些类。这是一个扁平的层次结构,实际上,关于列表,它根本不是一个层次结构


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

问题已经很好地解决了。但是有一个解决办法;使doSomething通用:

现在,您可以使用List或List或List调用doSomething。

子类型用于参数化类型。即使类Dog是Animal的子类型,参数化类型List也不是List的子类型。数组使用List子类型,因此数组 狗[]型是动物[]的一个亚型

不变子类型确保不违反Java强制执行的类型约束。考虑下面的代码@ @乔恩SKET:< /P>
List<Dog> dogs = new ArrayList<Dog>(1);
List<Animal> animals = dogs;
animals.add(new Cat()); // compile-time error
Dog dog = dogs.get(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");
    }
}
请注意,方法体的翻译代码是相同的。编译器将每个参数化类型替换为其参数化类型。此属性至关重要,这意味着它不会破坏向后兼容性


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

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

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?
调用List animals=new ArrayListdogs;之后;,随后不能将动物直接指定给狗或猫:

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


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

该问题已被正确识别为与差异相关,但细节不正确。纯函数列表是协变数据函子,这意味着如果类型子是超级的子类型,那么子列表肯定是超级列表的子类型

然而,列表的易变性并不是这里的基本问题。一般来说,问题是易变性。这个问题是众所周知的,被称为协方差问题,我认为它是由卡斯塔尼亚首先发现的,它彻底摧毁了面向对象作为一种普遍范式。它是基于以前建立的 由Cardelli和Reynolds建立的hed方差规则

有些过于简化,让我们考虑把类型T的对象B分配给类型T的对象A作为一个突变。这并没有失去普遍性:a的突变可以写成a=f a,其中f:T->T。当然,问题是,虽然函数在其余域中是协变的,但在其域中是逆变的,但随着赋值,域和余域是相同的,所以赋值是不变的

概括起来,亚型是不能变异的。但对于面向对象,变异是根本,因此面向对象本质上是有缺陷的

这里有一个简单的例子:在纯函数设置中,对称矩阵显然是一个矩阵,它是一个子类型,没有问题。现在,让我们向矩阵添加在坐标x,y处设置单个元素的功能,规则是其他元素不发生变化。现在对称矩阵不再是一个子类型,如果你改变了x,y,你也改变了y,x。函数操作是delta:Sym->Mat,如果你改变对称矩阵的一个元素,你会得到一个一般的非对称矩阵。因此,如果在Mat中包含一个change one element方法,则Sym不是子类型。事实上几乎可以肯定没有合适的亚型

更简单地说:如果您有一个通用数据类型,它具有广泛的变种,可以利用其通用性,您可以确定任何适当的子类型都不可能支持所有这些变种:如果可以,它将与超类型一样通用,这与适当子类型的规范相反

Java阻止对可变列表进行子类型化的事实并没有解决真正的问题:当Java在几十年前被怀疑时,为什么要使用像Java这样的面向对象垃圾

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


我看到这个问题已经被回答了好几次,我只想就同一个问题提出我的意见

让我们继续创建一个简化的动物类层次结构

abstract class Animal {
    void eat() {
        System.out.println("animal eating");
    }
}

class Dog extends Animal {
    void bark() { }
}

class Cat extends Animal {
    void meow() { }
}
现在让我们看看我们的老朋友数组,我们知道它隐式地支持多态性-

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的数组,也可以接受一个Dog数组,因为Dog-is-a-Animal。这就是多态性的作用

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

现在我们稍微调整一下代码,使用ArrayList而不是数组-

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());
        }
    }   
}
所以我们知道这是可行的,现在让我们稍微调整一下这个类,以多态方式使用动物类型-

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());
        }
    }   
}
认为这应该是可行的,因为理想情况下,它是一个动物ArrayList,您应该能够将任何猫作为猫也是动物添加到它,但实际上,您向它传递了一个狗类型的ArrayList

所以,现在您一定在想,同样的情况也应该发生在阵列上。你这样想是对的


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

还有一个完全不相关的语法问题现在困扰着我——我的标题应该是为什么Java不是泛型,或者为什么Java不是泛型??泛型是因为s的复数形式还是因为它是一个实体的单数形式?Java中的泛型是一种非常糟糕的参数多态形式。不要像我以前那样对他们抱有太多的信心,因为总有一天你会狠狠地打击他们可悲的局限性:外科医生扩展了可操作的、可操作的KABOOM!不计算[TM]。这是Java泛型的局限性。任何OOA/OOD都可以很好地转换为Java,使用Java接口可以很好地完成MI,但泛型不能解决这一问题。它们适用于集合和过程编程,大多数Java程序员都是这样做的……列表的超类不是列表,而是列表,即未知类型的列表。泛型删除编译代码中的类型信息。这样做是为了使使用泛型java 5及以上版本的代码与没有泛型的早期版本的java兼容。。。这肯定是为什么Java的泛型不是。。。。另一个问题是泛型实际上是一个形容词,所以泛型指的是一个被泛型修饰的复数名词。您可以说函数是泛型的,但这比说函数是泛型的要麻烦得多。然而,说Java有泛型函数和类,而不仅仅是Java有泛型,有点麻烦。作为一个写过关于形容词的硕士论文的人,我想你偶然发现了一个非常有趣的问题!可以说,数组协方差是一个语言设计缺陷。注意,由于类型擦除,generi在技术上不可能实现相同的行为
c集合。我想说泛型的全部要点是它不允许。。你永远也不能确定:因为它看起来很有趣,所以每一张狗的名单实际上就是一张动物的名单,就像直觉告诉我们的那样。关键是,并不是每个动物列表都是狗的列表,因此通过添加猫来改变列表是个问题。@Ingo:不,不是真的:你可以将猫添加到动物列表中,但不能将猫添加到狗的列表中。狗的列表只是一张动物列表,如果你认为它是只读的。“Jon Skest-----当然,但是谁在命令从猫和狗列表中做出新的列表实际上改变了狗的名单?在Java中,这是一个任意的实现决策。一个与逻辑和直觉背道而驰的东西。@Ingo:我肯定不会在一开始就用它。如果你有一张名单,上面写着我们可能想去的顶级酒店,然后有人在名单上加了一个游泳池,你认为这有效吗?不,这是一个酒店列表,而不是一个建筑物列表。这并不像我说的狗的列表不是动物的列表——我用代码的术语,用代码的字体。我真的不认为这里有任何歧义。使用子类无论如何都是不正确的-这是关于赋值兼容性,而不是子类化。@ruakh:问题是,您将某个可能在编译时被阻止的东西推到执行时。我认为数组协方差从一开始就是一个设计错误。这是一个好主意,它已经存在于JavaSE中了;是的,但是我定义的集合可以修改。是的,它可以修改。收集在4个版本中,2可能不正确。e、 g.我们不能称之为消费者、供应商,而狗是动物和Runnablet的子类型。这会产生比实际解决的问题更多的问题。如果你试图在列表中添加一只猫,肯定会产生问题,但出于循环的目的,我认为这是唯一不冗长的答案。是的,人类语言更模糊。但是,一旦你将不同的动物添加到狗的列表中,它仍然是一个动物列表,但不再是狗的列表。区别在于,一个有模糊逻辑的人,通常没有问题意识到这一点。作为一个发现数组的常量比较更令人困惑的人,这个答案对我来说是正确的。我的问题是语言直觉。我认为混淆源于以下问题:woozle的术语列表是指一个可以用来存储woozle的容器,一个可以容纳每个woozle的容器的容器,还是一个woozle容器的内容,一个woozle容器的内容,或者woozle容器的集合内容。woozles的英文短语列表通常指的是最后一个,但编程语言中的相关结构通常指的是另一个。有人可能会认为,正因为如此,协变数组是编译器的一个特性。
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());
// 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
// These are both illegal
dogs = animals;
cats = animals;
abstract class Animal {
    void eat() {
        System.out.println("animal eating");
    }
}

class Dog extends Animal {
    void bark() { }
}

class Cat extends Animal {
    void meow() { }
}
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
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
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());
        }
    }   
}
animals.add(new Cat());