接着上次的问题,我先来看看Collections类的sort()方法。
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}
疑惑:JDK的设计者们为什么把Collections<T>
中方法public static <T> void sort(List<T> list, Comparator<? super T> c)
里的比较器泛型要定义成<? super T>
而不是直接<T>
。按照我们直观的理解,我们就是对list里的每个T类型的元素排序,比较器里的类型直接写成T不就可以了吗?实际上,设计者是这样来考虑的。
思路:假设有这么一个Student类Student implements A, B, C ,因为他实现了A,B,C三个接口,所以我们可以按接口A, B或C中任何一个的特性去排序,但是我比较完之后最终返回的一定是List<Student>
这种类型的。我们定义Comparator<A> c
,就是按A类型排序;定义Comparator<C> c
,就是按C类型排序。也就是我们可以按自己的类型(Student),或者是他的父类型(A, B, C)这种方式来去进行比较,比较的点或者参照物是不一样的,但我们最终比较完之后返回回来的一定List<Student>
这种类型的。 所以<? super T>
比<T>
从语义上看更宽泛一些。
其实public static <T> void sort(List<T> list, Comparator<? super T> c)
里的泛型Comparator<? super T> c
语义上更宽泛一些,但实际上比较的就是T类型本身。
现在我们要对list排序,由于list集合里面的元素是String类型的,所以我们的比较器只能是以String类型,或者String更高的类型去比较list这个集合里的元素(这符合我们刚才说的那个泛型的语义化)。
那么编译器他这怎么能推断出这个item一定是个String呢?
按上面说的,这里的类型其实可以是String(< T>),也可以是String之上的类型(< ? super T>)如:Serializable,但是在我们没指定类型时候,编译器应该默认认为是String(< T>)。
这就是为什么,下面这lambda表达式可以直接推断出参数item的类型为String。
public class MyComparatorTest2 {
public static void main(String[] args) {
List<String> list = Arrays.asList("nihao", "hello", "world", "welcome");
//按字符串长度排序
Collections.sort(list, (item1, item2) -> item1.length() - item2.length() );
list.forEach(System.out::println);
}
}
再看之前报错推断不出类型参数的那句代码:
这里要注意的是,我们这里的比较器是多层的,comparingInt
返回一个比较器在调用reversed
再返回一个比较器,最终我们需要的比较器是reversed
返回的。第一个例子的lambda表达式就是一个比较器,我们直接就传入,java编译器很容易就推断出这个比较器里的类型参数。而第二个例子的lambda表达式仅仅作为第一层比较器comparingInt
的参数传进去,它离这个上下文已经很远了,所以它对上下文的感知是很弱很远的,不能感知到集合list里的元素类型是什么!它就只能推断成Object类型。
我们可以验证一下,很简单,现在它是2层,lambda表达式在第一层,我们把reversed去掉,不在第一层了吗?按理java就能感知到它的类型了。
的确如此,现在编译器就可以推断出类型参数了。说明java编译器它能感知离上下文最近的,第二个reversed
离上下文最近,我们可以推断它返回的比较器的类型参数,没问题,但是comparingInt(ToIntFunction<? super T> keyExtractor)
里的参数ToIntFunction<? super T> keyExtractor
离上下文很远了,无法推断其参数的类型参数。
Java的类型推断跟上下文很有关系。