什么是通配符<? super T>的实际应用示例?

77
我知道<? super T>代表任何T的超类(T的任意级别的父类)。但我确实很难想象出这个通配符的真实例子。
我理解<? super T>的含义,并且我见过这个方法:
public class Collections {
  public static <T> void copy(List<? super T> dest, List<? extends T> src) {
      for (int i = 0; i < src.size(); i++)
        dest.set(i, src.get(i));
  }
}

我正在寻找一个实际应用的例子,来说明这种结构的使用方法,而不是解释它是什么。

18
这不是一个副本,而是一个非常合理的问题。 - Eugene
6
我认为这不是重复的问题,他在询问具体情况,而不是背后的原则。 - ItFreak
2
这是我即将写下的答案,但却被关闭了。在某种程度上,我同意那些投票关闭的人:通过一些刻苦努力,可以从 https://dev59.com/7nE85IYBdhLWcg3wikK- 中推导出答案。然而,这个问题(以及其答案)着重于技术原则。一个简单、现实的例子说明何时使用? super T可能会很有帮助。 - Marco13
4
不认为这个问题应该被关闭为重复,因为作者要求的是面向对象编程的现实世界模型,而不是深入解释Java中继承工作原理的内容。 - John Stark
3
这个例子难道不是一个真实的使用案例吗? - user253751
显示剩余6条评论
9个回答

51

The easiest example I can think of is:

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

从相同的Collections中获取。这样,Dog可以实现Comparable<Animal>,如果Animal已经实现了它,Dog就不必做任何事情。 真实示例的编辑: 在一些电子邮件来回后,我被允许介绍一个来自我的工作场所的真实示例(耶!)。
我们有一个名为Sink的接口(它做什么并不重要),该接口的想法是累积东西。声明非常平凡(简化版):
interface Sink<T> {
    void accumulate(T t);
}

显然,有一个帮助方法可以接受一个List,并将其元素传递到一个Sink中(它有点更复杂,但为了使其简单):
public static <T> void drainToSink(List<T> collection, Sink<T> sink) {
    collection.forEach(sink::accumulate);
}

这很简单,对吧?可是...

我可以有一个List<String>,但我想将它排空到一个Sink<Object>中 - 对我们来说,这是一件相当常见的事情;但是这会失败:

Sink<Object> sink = null;
List<String> strings = List.of("abc");
drainToSink(strings, sink);

为了使这个工作起来,我们需要将声明更改为:
public static <T> void drainToSink(List<T> collection, Sink<? super T> sink) {
    ....
}

1
Kotlin 教程 以“Source”为例,基本上是你“Sink”示例的对偶。 - Bakuriu
2
你肯定可以通过将列表定义为 ? extends T 来以相反的方式定义它。 - Weckar E.
@WeckarE。不是如果该方法本身在列表中。 - Reinstate Monica
1
@WeckarE。我可以,但这会稍微改变语义,现在我无法向该“列表”添加任何内容,它仅限于生产者;正如所说,这只是一个简化的示例... - Eugene

16

假设你有这个类层次结构:猫继承自哺乳动物,而哺乳动物又继承自动物。

List<Animal> animals = new ArrayList<>();
List<Mammal> mammals = new ArrayList<>();
List<Cat> cats = ...

这些调用是有效的:

Collections.copy(animals, mammals); // all mammals are animals
Collections.copy(mammals, cats);    // all cats are mammals
Collections.copy(animals, cats);    // all cats are animals
Collections.copy(cats, cats);       // all cats are cats 

但是这些呼叫并不有效:

Collections.copy(mammals, animals); // not all animals are mammals
Collections.copy(cats, mammals);    // not all mammals are cats
Collections.copy(cats, animals);    // mot all animals are cats

因此,方法签名仅确保您从更具体的类(在继承层次结构中较低)复制到更一般的类(在继承层次结构中较高),而不是相反。


2
“super”关键字对于此操作并非必需。以下签名也将具有相同的行为:public static <T> void copy(List<T> dest, List<? extends T> src) { - Nick
1
@Nick 很好的发现。那么这个签名是为什么呢?这个问题应该提给Java语言设计者。到目前为止,我找到的唯一原因是你可以编写如下代码: Collections.<Mammal>copy(animals, cats); - 但我不知道为什么有人会/应该编写这样的代码... - Benoit

6
例如,查看Collections.addAll方法的实现:
public static <T> boolean addAll(Collection<? super T> c, T... elements) {
    boolean result = false;
    for (T element : elements)
        result |= c.add(element);
    return result;
}

在这里,元素可以插入到任何元素类型是元素类型T的超类型的集合中。

没有下限通配符:

public static <T> boolean addAll(Collection<T> c, T... elements) { ... }

以下内容将会是无效的:
List<Number> nums = new ArrayList<>();
Collections.<Integer>addAll(nums , 1, 2, 3);

因为术语 Collection<T>Collection<? super T> 更为严格。


另一个例子:

Java中的Predicate<T>接口,在以下方法中使用<? super T>通配符:

default Predicate<T> and(Predicate<? super T> other);

default Predicate<T>  or(Predicate<? super T> other);

<? super T> 允许链接更广泛的不同谓词,例如:

Predicate<String> p1 = s -> s.equals("P");
Predicate<Object> p2 = o -> o.equals("P");

p1.and(p2).test("P"); // which wouldn't be possible with a Predicate<T> as a parameter

我认为这个例子并不特别有说服力。如果你省略类型变量的显式实例化,你仍然可以使用任一方法签名编写 Collections.addAll(nums, 1, 2, 3) - Nick

2

Suppose you have a method:

passToConsumer(Consumer<? super SubType> consumer)

然后您可以使用任何可以消费SubTypeConsumer调用此方法:

passToConsumer(Consumer<SuperType> superTypeConsumer)
passToConsumer(Consumer<SubType> subTypeConsumer)
passToConsumer(Consumer<Object> rootConsumer)

举个例子:

class Animal{}

class Dog extends Animal{

    void putInto(List<? super Dog> list) {
        list.add(this);
    }
}

所以我可以将 Dog 放入 List<Animal>List<Dog> 中:

List<Animal> animals = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();

Dog dog = new Dog();
dog.putInto(dogs);  // OK
dog.putInto(animals);   // OK

如果将putInto(List<? super Dog> list)方法更改为putInto(List<Animal> list):
Dog dog = new Dog();

List<Dog> dogs = new ArrayList<>();
dog.putInto(dogs);  // compile error, List<Dog> is not sub type of List<Animal>

or putInto(List<Dog> list):

Dog dog = new Dog();

List<Animal> animals = new ArrayList<>();
dog.putInto(animals); // compile error, List<Animal> is not sub type of List<Dog>

1
每当你说“Consumer”或“consume”时,这自动属于“PECS”类别,不说这个也没关系,好的例子。 - Eugene

1
我写了一个网络广播,所以我有一个叫做MetaInformationObject的类,它是PLS和M3U播放列表的超类。我有一个选择对话框,所以我有:
public class SelectMultipleStreamDialog <T extends MetaInformationObject>
public class M3UInfo extends MetaInformationObject
public class PLSInfo extends MetaInformationObject

这个类有一个方法public T getSelectedStream()。因此,调用者收到了一个具体类型的T(PLS或M3U),但需要在超类上工作,因此有一个列表:List<T super MetaInformationObject>,结果被添加到其中。这就是通用对话框如何处理具体实现的方式,其余代码可以在超类上工作。希望这能让它更清晰一些。

1
考虑这个简单的例子:
List<Number> nums = Arrays.asList(3, 1.2, 4L);
Comparator<Object> numbersByDouble = Comparator.comparing(Object::toString);
nums.sort(numbersByDouble);

希望这个例子还算有说服力:你可以想象要对数字进行排序以便显示(这时 toString 是一个合理的排序方式),但是 Number 本身不具备可比性。
这段代码能够编译通过是因为 integers::sort 接受的是 Comparator。如果它只接受 Comparator(在这种情况下,E 指的是 Number),那么这段代码将无法编译通过,因为 Comparator 不是 Comparator 的子类型(原因是你已经理解,所以我不再详细解释)。

1

这里以集合为例。

1所述,List<? super T> 允许你创建一个可以容纳类型不超过 T 的元素的 List,因此它可以容纳从 T 继承的元素、类型为 T 的元素和 T 继承的元素。

另一方面,List<? extends T> 允许你定义一个只能容纳从 T 继承的元素的 List(在某些情况下甚至不能是类型为 T 的元素)。

这是一个很好的例子:

public class Collections {
  public static <T> void copy(List<? super T> dest, List<? extends T> src) {
      for (int i = 0; i < src.size(); i++)
        dest.set(i, src.get(i));
  }
}

在这里,您想将较少派生类型的List投影到较少派生类型的List。 在这里,List<? super T>向我们保证了来自src的所有元素都将在新集合中有效。

1 : Java中<? super T>和<? extends T>之间的区别


0

假设你有:

class T {}
class Decoder<T>
class Encoder<T>

byte[] encode(T object, Encoder<? super T> encoder);    // encode objects of type T
T decode(byte[] stream, Decoder<? extends T> decoder);  // decode a byte stream into a type T

接下来:

class U extends T {}
Decoder<U> decoderOfU;
decode(stream, decoderOfU);     // you need something that can decode into T, I give you a decoder of U, you'll get U instances back

Encoder<Object> encoderOfObject;
encode(stream, encoderOfObject);// you need something that can encode T, I give you something that can encode all the way to java.lang.Object

0
有几个现实生活的例子可以用来说明这一点。我想先提出的第一个例子是,现实世界中的物体被用于“即兴”功能。想象一下你有一个插座扳手:
public class SocketWrench <T extends Wrench>

套筒扳手的明显用途是作为一种扳手。然而,如果您考虑到在紧急情况下可以使用扳手敲入钉子,那么您可以拥有以下继承层次结构:
public class SocketWrench <T extends Wrench>
public class Wrench extends Hammer

在这种情况下,您可以调用socketWrench.pound(Nail nail = new FinishingNail()),即使这被认为是SocketWrench的非典型用法。
同时,如果将其用作SocketWrench而不仅仅是WrenchHammer,则SocketWrench将能够访问并调用方法,例如applyTorque(100).withRotation("clockwise").withSocketSize(14)

我觉得这并不令人信服:为什么SocketWrench会是一个通用的东西? - Max
你可以拥有各种类型的插座扳手:1/4英寸驱动器,1/2英寸驱动器,可调节角度插座扳手,扭矩扳手,不同齿轮数的棘轮扳手等等。但是,如果你只需要一个棘轮扳手,你可以使用通用的SocketWrench而不是特定的3/4英寸驱动器32齿角柄插座扳手。 - John Stark

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