Java泛型:在这里使用通配符有什么好处?

Java Generics: What is the benefit of using wildcards here?

Collections.fill方法有以下header:

public static <T> void fill(List<? super T> list, T obj)

为什么需要通配符?以下 header 似乎也有效:

public static <T> void fill(List<T> list, T obj)

我看不出需要通配符的原因;下面的代码适用于第二个 header 以及第一个:

List<Number> nums = new ArrayList<>();
Integer i = 43;
fill(nums, i); //fill method written using second header

我的问题是:对于 fill 的哪个特定调用,第一个 header 有效但第二个无效?如果没有这样的调用,为什么要包含通配符? 在这种情况下,通配符不会使方法更简洁,也不会增加可读性(我认为)。

对于您的示例,它 'works' 带有您的基本 <T> 签名的原因是整数也是数字。唯一可行的 'T' 是 T = Number,然后整个事情就解决了。

在这种情况下,T obj 参数的表达式是具体化类型:您有一个 Integer。您可以使用 T 代替。也许你有这个:

class AtomicReference<T> {
  // The actual impl of j.u.concurrent.AtomicReference...
  // but with this one additional method:

  public void fillIntoList(List<? super T> list) {
    T currentValue = get();
    Collections.fill(list, currentValue);
  }
}

我可能想写这样的东西:

AtomicReference<String> ref = new AtomicReference<String>("hello");
List<CharSequence> texts = new ArrayList<>();

...

ref.fillIntoList(texts);

如果我假设的 fillIntoList 方法只是在无法编译的签名中包含 List<T>。幸运的是它确实如此,所以代码确实可以编译。如果 Collections.fill 方法没有完成 <? super T> 事情,那么在我的 fillIntoList 方法中调用 Collections.fill 方法就会失败。

任何一个出现都是非常奇特的。但它可以出现。 List<? super T> 是这里绝对优越的签名 - 它可以做 List<T> 所做的一切,甚至更多,而且它在语义上也是正确的:当然,我可以通过在每个插槽中写入 a 来填充 foos 列表如果 bar 是 foo 的子对象,我确定它是 bar 的引用。

那是因为继承在某些情况下是有用的。

例如,如果您有以下 class 结构:

public class Parent {
  //some code
}

public class Child extends Parent {
  //some another code
}

你可以用第一种方法写:

List<Child> children = new ArrayList<>();
Parent otherParentObject = new Parent(); //after this line, set the values for the class
List<Parent> outParentList = new ArrayList<>();
fill(children, otherParentObject); //fill method using first signature;

这个问题问得真好,简单的答案已经猜到了:

For the current version of the fill(List<? super T> list, T obj) there is no such input that would be rejected given the signature is changed to fill(List<T> list, T obj), so there is no benefit and the devs are likely followed the PECS principle

上述说法的推导原理是:如果存在这样的类型X使得 XT 的超类型,然后 List<X>List<? super T> 的超类型,因为类型逆变。 由于我们总能找到这样的 X(在最坏的情况下是 Object class)- 编译器可以推断出合适的 List<X> 给定任何一种形式的参数类型 fill.

因此,知道这一事实后,我们可以干扰编译器并使用“类型见证”自行推断类型,从而使代码中断:

List<Object> target = new ArrayList<>();
//Compiles OK as we can represent List<Object> as List<? super Integer> and it fits
Collections.<Integer>fill(target, 1);

//Compilation error as List<Object> is invariant to List<Integer> and not a valid substitute
Collections.<Integer>fillNew(target, 1);

这当然是纯理论的,任何头脑正常的人都不会在那里使用类型参数。

然而

在回答“这里使用通配符有什么好处?”这个问题时,我们只考虑了等式的一方面——我们,方法的消费者和我们的经验,但是不是图书馆开发人员。

因此这个问题有点类似于为什么 Collections.enumeration(final Collection<T> c) 是这样声明的,而不是 enumeration(Collection<T> c) 因为 final 对最终用户来说似乎是多余的。

这里可以推测真实意图,但我可以给出一些主观原因:

  1. 首先:使用 List<? super T>(以及 enumerationfinal)立即消除了代码的歧义,特别是对于 <? super T> - 它对表明只有部分知识 类型参数是必需的,list 不能用于生成 T 的值,而只能用于使用它们。 引用:

Wildcards are useful in situations where only partial knowledge about the type parameter is required. JLS 4.5.1. Type Arguments of Parameterized Types

  1. 其次:它为库所有者提供了一些自由 improve/update 该方法,同时不破坏向后兼容性,同时符合现有约束。

现在让我们尝试进行一些假设的“改进”以了解我的意思(我将使用 List<T>fill 形式称为 fillNew):

#1 决定是将 return 的 obj 值(用于填充列表)返回:

public static <T> void fill(List<? super T> list, T obj)
//becomes ↓↓↓
public static <T> T fill(List<? super T> list, T obj)

更新后的方法对于 fill 签名来说工作得很好,但是对于 fillNew - 推断的 return 类型现在不是那么明显:

List<Number> target = new ArrayList<>();
Long val = fill(target, 1L); //<<Here Long is the most specific type that fits both arguments
//Compilation error
Long val = fillNew(target, 1L); //<<Here Number is, so it cannot be assigned back

//More exotic case:
Integer val = fill(asList(true), 0); //val is Integer as expected
Comparable<?> val = fillNew(asList(true), 0); //val is now Comparable<?> as the most specific type 

#2 添加 fill 的重载版本的决定,在 TComparable<T> 的情况下性能提高 10 倍:

/* Extremely performant 10x version */
public static <T extends Comparable<T>> void fill(List<? super T> list, T value)
/* Normal version */
public static void fill(List<? super T> list, T value)

List<Number> target = new ArrayList<>();
fill(target, 1);  //<<< Here the more performant version is used as T inferred to Integer and it implements Comparable<Integer>
fillNew(target, 1); //<< Still uses the slow version just because T is inferred to Number which is not Comparable
    

总而言之 - fill 的当前签名在我看来对各方(开发人员和库设计人员)来说更 flexible/descriptive