一个 List<? extends MyObject> 有什么好处?

5

给定一个简单的类,它表示一个人的年龄和姓名:

public class Parent {

    private final String value;

    public Parent(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return value;
    }

}

...以及它的一个子类...

public class Child extends Parent {
    public Child(String value) {
        super(value);
    }
}

以下程序可以编译并且执行没有错误或警告:
public class Main {

    public static void main(String[] args) {
        List<? extends Parent> items = getItems();
        for (Parent p : items) {
            System.out.println(p.toString());
        }
    }

    private static List<? extends Parent> getItems() {
        List<Parent> il = new ArrayList<Parent>();
        il.add(new Parent("Hello"));
        il.add(new Child("World"));
        return il;
    }
}

但是getItems()的返回类型怎么办?对我来说,这似乎在语义上是错误的,因为函数的结果确实包含一个不是Parent子类的Parent对象,这不符合该方法签名的预期。鉴于这种行为,我本来希望返回一个简单的List<Parent>类型。然而,由于编译器没有抱怨,我错过了什么吗?除了对结果列表进行(不必要的)“写入保护”之外,这种实现是否有任何好处?还是这是Java样式的结果列表写入保护(例如,在不需要创建复制品的情况下公开对象的字段列表)?还是这可以被认为是“错误”的(从教学角度来看)?如果是这样,那么为什么编译器甚至连Eclipse都没有抱怨它,甚至没有显示提示?


1
我认为你已经列出了好处 - 写保护。它是否在你特定的代码示例中需要是另一个争论,但肯定不应该有编译器或Eclipse警告你任何事情。 - Duncan Jones
我认为反过来看更有好处:读取保证。你知道可以将数据读取到一个类型为Parent的变量中。但你不知道 - TheConstructor
除了已经涵盖主要观点的答案(灵活性!)之外:1.ParentParent的子类,2.该列表不是“写保护”的:您仍然可以添加null(或删除元素)。 - Marco13
4个回答

4

List<A>是不变的。 List<? extends A>是协变的,List<? super A>是逆变的。

协变类型不能在没有强制转换的情况下进行修改(这是有道理的,因为可变集合本质上不是协变的)。

如果集合不需要被修改,则使用List<? extends A>获得适当的子类型关系(例如,List<? extends Integer>List<? extends Number>的子类型)是没有问题的。

我不建议将其看作“写保护”,因为规避它的强制转换非常简单。将其视为使用最通用的类型即可。如果您编写一个接受List<A>参数的方法,并且该方法永远不需要修改列表,则List<? extends A>是更通用和更合适的类型。

作为返回类型,使用协变也很有用。假设您正在实现一个返回类型为List<Number>的方法,并且您有一个要返回的List<Integer>。你会有些束手无策:要么复制它,要么做一个丑陋的强制转换。但是,如果您的类型是正确的,并且接口要求List<? extends Number>,那么您可以直接返回您拥有的东西。


((List<Whatever>) list).add(whatever) - Chris Martin
如果 IntegerNumber 的子类型,并不意味着 List<? extends Integer>List<? extends Number> 的子类型。 - webuster
是的 - 那正是我所说的吗? - Chris Martin
@ChrisMartin 好的,我以为你是这个意思。我不确定这是否真的很简单 - 它是未经检查的、脆弱的,并需要了解实际使用的类型。 - Duncan Jones
1
使用糟糕的标准收集库已经很脆弱,并且需要掌握具体类型的知识,因为“List”既用于可变列表也用于不可变列表。 - Chris Martin

3

您已经提到了“写保护”作为一项优点,这取决于使用场景,可能有用也可能没用。

另一个好处是在某种程度上具有一定的灵活性,因为您可以更改方法的实现并编写类似于以下内容的代码:

    List<Child> il = new ArrayList<Child>();
    il.add(new Child("Hello"));
    il.add(new GrandChild("World")); // assume there is such a thing
    List<? extends Parent> result = il;
    return result;

这会改变列表内容的类型,但保持接口(即公共方法签名)不变。如果你在写框架,这非常关键。


它必须是List<Child>(否则您甚至无法添加新元素),然后您不能添加GrandChild实例。我不小心点了赞(在注意到这个错误之前),因为这是我认为的主要原因:您可以返回一个List<Child>,它仍然被视为List<? extends Parent> - Marco13
我的错,从原始代码开始。现在应该没问题了。 - webuster

2

? extends X? super Y语法在处理子类中的专门类型列表时非常有用,您需要确保不混入无效对象。对于普通的ArrayList,您总是冒着未经检查的强制转换的风险,并规避限制,但也许这个示例说明了您可以如何使用参数化:

import java.util.AbstractList;
import java.util.List;

/**
 * https://dev59.com/CIPba4cB1Zd3GeqPmgRO
 */
public class ListTest {

    public static void main(String[] args) {
        ParentList parents = new ParentList(new Parent("one"), new Child("two"));
        List<? extends Parent> parentsA = parents; // Read-Only, items assignable to Parent
        List<? super Parent> parentsB = parents; // Write-Only, input of type Parent
        List<Parent> parentC = parents; // Valid as A and B were valid
        List<? super Child> parentD = parents; // Valid as you can store a Child
        // List<? extends Child> parentE = parents; // Invalid as not all contained values are Child

        ChildList children = new ChildList(new Child("one"), new Child("two"));
        List<? extends Parent> childrenA = children; // Read-Only, items assignable to Parent
        // List<? super Parent> childrenB = children; // Invalid as ChildList can not store a Parent
        List<? super Child> childrenC = children; // Write-Only, input of type Child
        // List<Parent> childrenD = children; // Invalid as B was invalid
        List<Child> childrenE = children;
    }


    public static class ChildList extends AbstractList<Child> {

        private Child[] values;

        public ChildList(Child... values) {
            this.values = values;
        }

        @Override
        public Child get(int index) {
            return values[index];
        }

        @Override
        public Child set(int index, Child element) {
            Child oldValue = values[index];
            values[index] = element;
            return oldValue;
        }

        @Override
        public int size() {
            return values.length;
        }
    }

    public static class ParentList extends AbstractList<Parent> {

        private Parent[] values;

        public ParentList(Parent... values) {
            this.values = values;
        }

        @Override
        public Parent get(int index) {
            return values[index];
        }

        @Override
        public Parent set(int index, Parent element) {
            Parent oldValue = values[index];
            values[index] = element;
            return oldValue;
        }

        @Override
        public int size() {
            return values.length;
        }
    }

    public static class Parent {

        private final String value;

        public Parent(String value) {
            this.value = value;
        }

        @Override
        public String toString() {
            return value;
        }

    }

    public static class Child extends Parent {
        public Child(String value) {
            super(value);
        }
    }
}

关于命名的最后说明:

  • ? extends X解读为“X或任何扩展X的类型”或“可分配给类型为X的变量的某些内容”
  • ? super X解读为“X或任何类型X扩展”的内容,或者“我可以将类型为X的变量分配给某些内容”。

你试图解释 ? extends? super 的工作原理是可以的,但列表不会变成“只读”。列表仍然可以被修改(只是不能用错误的类型填充)。 - Marco13
@Marco13 如果没有进一步的转换,您将无法调用set。因此,在技术上它不是只读的,但实际上它变成了只读的。如果您不使用反射,则可以使用Collections.unmodifieableList()使其变为“只读”......否则:更难写出我选择称其为只读的原因。 - TheConstructor
list.set(0, null)仍然有效。但是,一般来说,列表可以/应该作为Collections.unmodifiableList公开 - 在这种情况下,类似List<Parent> p = Collections.unmodifiableList(childList)的代码也能正常工作。 - Marco13
我通常会把它们合并起来,像 List<? extends Parent> p = Collections.unmodifiableList(childList) 一样,这样你的编译器和运行时都会显示“只读”。 - TheConstructor

-1

当你说 List<? extends Parent> 时,它意味着任何 Parent 类型的对象或其任何子类。我同意这有点误导人,但这就是它的工作方式。

我可以看到这种模式的一个潜在好处是在 main 函数中使用反射 API 时。

假设你的函数返回 List<Parent>,并且在你的 main() 中,如果你执行以下操作:

private static List<Parent> getItems() { ... }

public static void main(String[] args) {
    List<? extends Parent> items = getItems();
    for (Parent p : items) {
        System.out.println(p.getClass().getSimpleName());
    }
}

会给你产生

Parent
Parent

如果你有

private static List<? extends Parent> getItems() { ... }

主函数将会打印

Parent
Child

2
-1 这不正确。对我来说这似乎很奇怪,所以我测试了一下代码,结果在两种情况下都打印出 Parent / Child - Duncan Jones
当你意识到通用类型在编译后基本上被擦除时,你就知道如果Java想要做到这一点,它甚至都做不到... - TheConstructor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接