今天当我偶然间看到一个很常用的Java静态方法时它的函数签名引起了我的注意
public static <T extends Comparable<? super T>> void sort(List<T> list)
其中对于泛型参数的定义让我有些疑惑,既然是排序方法那么只需要实现Comparable接口就好。但是为什么后面Comparable接口中的泛型参数还要以泛型T为下界呢?
抱着这个疑惑回顾了一下Java中关于类型通配符,协变,逆变等概念的回顾并未得到一个合理的解释。在我看来
public static <T extends Comparable<T>> void sort(List<T> list)
跟之前的函数签名并没有区别。
但是我知道JDK源码必定是字斟句酌,并非如此简单。于是我打算努力写出一个例子来表现出两个函数签名之间的区别。
List<Integer> list = new ArrayList<>();
Collections.sort(list);
但是很显然它失败了。思索再三,我又下了如下代码。
public class Main1 {
public static void main(String[] args) {
List<AA> x = new ArrayList<>();
sort(x);
}
static <T extends Comparable<? super T>> void sort(List<T> list) {
}
}
class AA extends BB {
}
class BB implements Comparable<BB> {
@Override
public int compareTo(BB o) {
return 0;
}
}
然后当我将自定义方法中的函数签名修改为
static <T extends Comparable<T>> void sort(List<T> list)
IDE果然给出了编译错误。到这里按说应该结束了。毕竟已经解决掉了疑惑。但是关于泛型中extends和super的使用却让我难以确定,到底我该遵从什么样的原则来进行函数签名的设计呢?答案就是get-put原则。
*The get-put principle
Use an extends wildcard when you only get values out of a structure, use a super wildcard when you only put values into a structure, and don’t use a wildcard when you do both.*
get-put原则的描述来源于Naftalin和Wadler合作的一本书,Java Generics and Collections,简单翻译如下
当需要从某种结构中获取值时使用extends通配符,当需要向某种结构中存放值时采用super通配符。当既需要存值又需要取值时不要使用通配符。
该原则简单易懂,以此为准则分析上述Collections.sort()的函数签名会发现,当我们排序时可看作需要从实现Comparable接口的众多实现类中获取我们要排序的类型故前半部分的签名采用extends通配符。而当我们进行排序时需要把排序的逻辑放到对应的排序方法中,故后半部分采用了super通配符。看起来该函数签名似乎陷入了某种循环
T extends Comparable<? super T>
事实上并没有。我们需要排序的是实现Comparable接口的类型。但实现该类型并非一定要自己实现Comparable接口,也可以通过继承某些实现了Comparable接口的类型来达到相同的目的。
除此之外,get-put原则最后部分描述的内容也很好理解了。如果既要获取泛型T或者其子类型的值又要存入T类型或者其父类型的值,那么当前的类型必定是只能是T本身,所以自然不需要使用任何类型通配符啦!
参考链接:https://www.ibm.com/developerworks/java/library/j-jtp07018/index.html?ca=drs-