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
使得
X
是 T
的超类型,然后 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
对最终用户来说似乎是多余的。
这里可以推测真实意图,但我可以给出一些主观原因:
- 首先:使用
List<? super T>
(以及 enumeration
的 final
)立即消除了代码的歧义,特别是对于 <? 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
- 其次:它为库所有者提供了一些自由 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
的重载版本的决定,在 T
为 Comparable<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
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 tofill(List<T> list, T obj)
, so there is no benefit and the devs are likely followed the PECS principle
上述说法的推导原理是:如果存在这样的类型X
使得
X
是 T
的超类型,然后 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
对最终用户来说似乎是多余的。
这里可以推测真实意图,但我可以给出一些主观原因:
- 首先:使用
List<? super T>
(以及enumeration
的final
)立即消除了代码的歧义,特别是对于<? 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
- 其次:它为库所有者提供了一些自由 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
的重载版本的决定,在 T
为 Comparable<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