Java中Comparator.comparing方法的泛型语义及通配符疑问解析
这是个非常经典的泛型设计问题,核心是Java泛型的逆变、协变和PECS原则,咱们挨个拆解:
1. 为什么是Comparable<? super U>而非Comparable<U>?
简单说:为了支持U的超类已经实现了Comparable的场景,避免不必要的重复实现。
举个实际的例子:
// 父类Animal实现了Comparable<Animal> class Animal implements Comparable<Animal> { private int age; public Animal(int age) { this.age = age; } @Override public int compareTo(Animal other) { return Integer.compare(this.age, other.age); } } // Dog继承Animal,没有自己实现Comparable class Dog extends Animal { public Dog(int age) { super(age); } }
如果comparing方法的签名是Comparable<U>,当我们想按年龄比较Dog对象时:
// 这里U是Dog,但Dog并没有实现Comparable<Dog>,只继承了Animal的Comparable<Animal> // 这行代码会编译失败! Comparator<Dog> dogComparator = Comparator.comparing(dog -> dog);
但换成Comparable<? super U>就完全没问题:
// U是Dog,? super Dog包含Animal,而Animal实现了Comparable<Animal>,符合要求 Comparator<Dog> dogComparator = Comparator.comparing(dog -> dog);
因为Dog是Animal的子类,用Animal的compareTo逻辑来比较Dog完全合理——这正是泛型逆变的价值:允许使用超类的兼容实现,提升代码复用性。
2. 为什么是Function<? super T, ? extends U>而非Function<T, U>?
这里要用到Java泛型的PECS原则(Producer Extends, Consumer Super):
- 对于输入参数(Function的入参,也就是消费T的场景),用
? super T(逆变) - 对于输出结果(Function的返回值,也就是生产U的场景),用
? extends U(协变)
先看? super T的作用:允许复用超类的提取器
比如我们有一个通用的提取器,针对Animal提取年龄:
Function<Animal, Integer> animalAgeExtractor = Animal::getAge;
如果comparing用的是Function<T, U>,那这个提取器不能直接用来处理Dog(因为T是Dog,而Function<Animal, Integer>不是Function<Dog, Integer>)。但用? super T就可以:
// T是Dog,? super Dog包含Animal,所以Function<Animal, Integer>符合要求 Comparator<Dog> dogAgeComparator = Comparator.comparing(animalAgeExtractor);
这样我们就能复用为超类写的提取逻辑,不用为每个子类重复写一遍。
再看? extends U的作用:允许返回更具体的类型
假设我们有个提取器,返回的是Integer(U是Number的子类):
Function<Dog, Integer> dogAgeExtractor = Dog::getAge;
如果comparing用的是Function<T, U>,当我们想把U指定为Number时,Function<Dog, Integer>不会被接受(泛型默认是不变的,Function<Dog, Integer>不是Function<Dog, Number>)。但用? extends U就可以:
// U是Number,? extends Number包含Integer,所以Function<Dog, Integer>符合要求 Comparator<Dog> dogAgeComparator = Comparator.comparing(dogAgeExtractor, Comparator.naturalOrder());
这允许提取器返回U的任意子类,只要它能被U的Comparable逻辑处理就行,进一步提升了灵活性。
总结
这两个通配符的设计都是为了在保证类型安全的前提下,最大化comparing方法的适用范围——让它能接受更多合法的参数,同时避免不必要的代码重复。
内容的提问来源于stack exchange,提问作者Tarun

