【问题标题】:Why does Java have lower bounds in generics?为什么 Java 在泛型中有下界?
【发布时间】:2016-03-01 04:04:05
【问题描述】:

我正在尝试设计自己的编程语言,并且正在考虑泛型。我做 Java 已经有一段时间了,知道 extendssuper 泛型边界。

我正在阅读this 的帖子并试图了解对下限的需求。

在我的语言中,我打算像普通字段一样使用泛型,如果你说List<MyObject>,你可以存储MyObject,或MyObject 的任何子类型。有道理吗?

在帖子中,它们具有以下类层次结构:

class Person implements Comparable<Person> {
  ...
}

class Student extends Person {
  ...
}

然后他们有一个排序方法:

public static <T extends Comparable<T>> void sort(List<T> list) {
  ...
}

我认为,您应该能够向此方法发送List&lt;Student&gt;。作为Student 扩展Personcompare 方法将由它的超类Person 处理。

错误消息的原因是编译器将排序方法的类型参数推断为 T:=Student 并且 Student 类不是 Comparable&lt;Student&gt; 。是Comparable&lt;Person&gt;,但是不符合方法排序的类型参数的界限的要求。要求T(即Student)是Comparable&lt;T&gt;(即Comparable&lt;Student&gt;),其实不是。

以上对我来说没有任何意义......你应该可以做到student.compare(person),那么为什么这不起作用?

也许是说Student 应该实现它自己的可比较方法,以便Student 在比较中有发言权?你不需要做任何特别的事情,只需覆盖Person 的方法。您无法保证您正在与另一个 Student 进行比较,但可以通过 instanceof 进行检查。

这里有什么我遗漏的吗?

经过这么多思考,我现在想知道extends 的目的是什么。据我了解,在List&lt;MyType&gt; 中,您只能放入MyType,而不是它的任何子类。如上所述,这对我来说没有任何意义,您应该能够将任何子类像字段一样放在列表中。

我可能应该说清楚,这不是“为什么它在 Java 中不起作用”,而是“为什么它在泛型理论中不起作用”。我只是标记了 java,因为这是我进行比较的地方。

【问题讨论】:

  • in a List&lt;MyType&gt;, you can only put a MyType in, not any of it's subclasses 不正确。
  • 那么extends的目的是什么?
  • 如果您需要深入了解 Java 泛型,那么您的第一站应该是这个常见问题解答,angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html
  • “作为 Student 扩展 Person,比较方法将由它的超类 Person 处理。” 如果您推断 T=Person,那么您正在尝试转换 @987654353 @ 到 List&lt;Person&gt; 这是不允许的。 “为什么我不能用 Java 泛型做 X?” 形式的几乎所有问题的答案是 X 可能导致某种失败。

标签: java language-design


【解决方案1】:

第一:方法声明

public static <T extends Comparable<T>> void sort(List<T> list)

对我来说没有多大意义。我觉得应该是

public static <T extends Comparable<? super T>> void sort(List<T> list)

那么就可以写sort(listOfStudents)。现在我将解释上界下界通配符的优势:


类型参数的多态性不会转移到它的泛型类型

这意味着学生列表 (List&lt;Student&gt;) 不是人员列表 (List&lt;Person&gt;)。类似的指令

List<Person> list = new List<Student>();

在 Java 中会失败。原因很简单:list.add(new Person()); 对于学生列表是非法的,但对于人员列表则不是。

上界通配符

但也许你有一个不关心对象是否是子类的函数。例如:你可以有这样的方法:

void printAll(List<Person> list)

他们只是将所有人的一些数据打印到标准输出。如果你有学生名单 (List&lt;Student&gt; listOfStudents),你可以写:

List<Person> listOfPersons = new ArrayList<>();
for (final Student student : listOfStudents) {
    listOfPersons.add(student);
}
printAll(listOfPersons);

但您可能会发现这不是一个很好的解决方案。另一种解决方案是为printAll 使用上界通配符

void printAll(List<? extends Person> list)

您可以在printAll 中写类似Person person = list.get(0) 的内容。但是你不能写print.add(new Person()),因为list可能是学生名单或其他东西。

下界通配符

现在在另一个方向上也是一样的:假设你有一个函数可以生成一些学生并将他们放在一个列表中。像这样的:

void generateStudents(List<Student> list) {
    for (int i = 0; i < 10; ++i) {
        list.add(new Student());
    }
}

现在您有一个人员列表 (List&lt;Person&gt; listOfPersons),并希望在此列表中生成学生。你可以写

List<Student> listOfStudents = new ArrayList<>();
generateStudents(listOfStudents);
for (Student student : listOfStudents) {
    listOfPersons.add(student);
}

您可能会再次看到,这不是一个很好的解决方案。您还可以将generateStudents 的声明更改为

void generateStudents(List<? super Student> list)

现在,你可以写generateStudents(listOfPersons);

【讨论】:

  • "方法声明没有意义,应该是"。确切地。这也是标准库Collections#sort 方法的签名。 docs.oracle.com/javase/7/docs/api/java/util/…
  • 好吧,我想我现在明白其中的道理了。也许允许List&lt;MySubType&gt; x = new ArrayList&lt;MyType&gt;() 是有意义的?而不是需要? super MySubType?
  • @chris13524 我认为这不是一个好主意:“原因很简单:list.add(new Person()); 将是非法的学生,但不是人员列表”。我知道这有点令人困惑,因为在自然语言中,您会将 “学生列表” 称为 “人员列表”。但是您必须考虑到,在可以修改列表的上下文中,这是不正确的。学生列表应仅包含学生。如果它也是人员列表,您可以添加人员。您甚至可以将任何对象添加到列表中,因为它也是一个对象列表。
  • @chris13524 我的东西 C# 没有这样的概念。要允许上述函数,您可以将List&lt;Student&gt;List&lt;Object&gt; 的实例传递给需要List&lt;Person&gt; 的函数。该函数无法说出哪个是正确的。假设您有一个函数generatePersons(List&lt;Person&gt;),并且您通过了List&lt;Student&gt;。编译器不知道这是错误的。该函数可以写入列表或从中读取或两者兼而有之。但是,只要您想从学生列表中读取学生,就会抛出异常,因为它也包含 Persons。
  • 是的,我明白了,我只是希望有更好的方法来实现它。一种像字段一样易于理解的方式。
【解决方案2】:

我认为您的困惑可能来自这样一个事实,即 List&lt;Student&gt; 的元素可以通过以下事实相互比较,因为类 Student 子类 Person 实现了 Comparable&lt;Person&gt;(类 Student因此继承了compareTo(Person o),可以用Student的实例调用),你仍然不能用List&lt;Student&gt;调用sort方法...

问题是当Java编译器遇到语句时:

sort(studentList); 

其中studentList是参数化类型List&lt;Student&gt;的一个实例,它使用类型推断来推断sort方法T的类型参数是Student,而Student不满足上绑定:Student extends Comparable&lt;Student&gt;。因此,这种情况下编译器会抛出错误,告诉你推断的类型不符合约束。

您链接到的文章向您展示了解决此问题的方法是将排序方法重写为:

public static <T extends Comparable <? super T > > void sort(List<T> list)

此方法签名放松了对类型参数的约束,以便您可以使用List&lt;Student&gt; 调用该方法。

我对你帖子的最后一部分不太清楚:

List&lt;MyType&gt; 中,您只能放入MyType,而不是它的任何子类。

如果您指的是List&lt;MyType&gt; 的元素,是的,您可以将任何属于MyType 子类型的元素放在此列表中,例如MySubType。如果您使用List&lt;MyType&gt; 作为其引用类型来引用变量,那么不,您不能将对List&lt;MySubType&gt; 的引用放在List&lt;MyType&gt; 类型的变量中。当您考虑以下情况时,很容易看出其原因:

List<MyType> a;
List<MySubType> b = new ArrayList<>();
a = b; // compile-time-error, but assume OK for now
a.add(new MyType()); // Based on the type of a, this should be OK, but it's not because a is actually a reference to List<MySubType>.

【讨论】:

    【解决方案3】:

    我还认为您应该参考 Wadler 和 Naftalin 的 Java Generics,这是对 Java (5+) 类型系统的出色介绍。

    当您问“extends 关键字的目的是什么?”根据您对对象集合的观察,您应该记住的第一件事是通用集合很棘手。我引用了 Wadler/Naftalin 的书(重点是我的):

    在 Java 中,一种类型是另一种类型的 子类型,如果它们由 extends 或 implements 子句:Integer 是 Number 的子类型。 子类型是可传递的。

    如果 A 是 B 的子类型,则 B 是 A 的超类型。

    Liskov 的替换原则告诉我们,只要有一个 type 是预期的,可以提供该类型的任何子类型的值: 给定类型的变量可以被赋予任何子类型的值 该类型,并且具有给定类型参数的方法可能是 使用该类型的任何子类型的参数调用。

    这是因为违反了 Liskov 的替换原则(即 在实践中会很快出现)List 不是 List 的子类型,尽管 Integer 是 Number 的子类型。这 其他方式也不起作用,因为 List 不是 List 的子类型。

    这一段应该可以帮助我们理解为什么关键字extends 对支持继承和多态是必不可少的,但它(在某种程度上)以泛型集合的方式出现。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-06-21
      • 2013-11-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多