Java 8 Comparator comparing static function
Here is a simple example: comparing cars by weight. I will first describe the problem in text-form, and then demonstrate every possible way how it can go wrong if either ? extends
or ? super
is omitted. I also show the ugly partial workarounds that are available in every case. If you prefer code over prose, skip directly to the second part, it should be self-explanatory.
Informal discussion of the problem
First, the contravariant ? super T
.
Suppose that you have two classes Car
and PhysicalObject
such that Car extends PhysicalObject
. Now suppose that you have a function Weight
that extends Function<PhysicalObject, Double>
.
If the declaration were Function<T,U>
, then you couldn't reuse the function Weight extends Function<PhysicalObject, Double>
to compare two cars, because Function<PhysicalObject, Double>
would not conform to Function<Car, Double>
. But you obviously want to be able to compare cars by their weight. Therefore, the contravariant ? super T
makes sense, so that Function<PhysicalObject, Double>
conforms to Function<? super Car, Double>
.
Now the covariant ? extends U
declaration.
Suppose that you have two classes Real
and PositiveReal
such that PositiveReal extends Real
, and furthermore assume that Real
is Comparable
.
Suppose that your function Weight
from the previous example actually has a slightly more precise type Weight extends Function<PhysicalObject, PositiveReal>
. If the declaration of keyExtractor
were Function<? super T, U>
instead of Function<? super T, ? extends U>
, you wouldn't be able to make use of the fact that PositiveReal
is also a Real
, and therefore two PositiveReal
s couldn't be compared with each other, even though they implement Comparable<Real>
, without the unnecessary restriction Comparable<PositiveReal>
.
To summarize: with the declaration Function<? super T, ? extends U>
, the Weight extends Function<PhysicalObject, PositiveReal>
can be substituted for a Function<? super Car, ? extends Real>
to compare Car
s using the Comparable<Real>
.
I hope this simple example clarifies why such a declaration is useful.
Code: Full enumeration of the consequences when either ? extends
or ? super
is omitted
Here is a compilable example with a systematic enumeration of all things that can possibly go wrong if we omit either ? super
or ? extends
. Also, two (ugly) partial work-arounds are shown.
import java.util.function.Function;
import java.util.Comparator;
class HypotheticComparators {
public static <A, B> Comparator<A> badCompare1(Function<A, B> f, Comparator<B> cb) {
return (A a1, A a2) -> cb.compare(f.apply(a1), f.apply(a2));
}
public static <A, B> Comparator<A> badCompare2(Function<? super A, B> f, Comparator<B> cb) {
return (A a1, A a2) -> cb.compare(f.apply(a1), f.apply(a2));
}
public static <A, B> Comparator<A> badCompare3(Function<A, ? extends B> f, Comparator<B> cb) {
return (A a1, A a2) -> cb.compare(f.apply(a1), f.apply(a2));
}
public static <A, B> Comparator<A> goodCompare(Function<? super A, ? extends B> f, Comparator<B> cb) {
return (A a1, A a2) -> cb.compare(f.apply(a1), f.apply(a2));
}
public static void main(String[] args) {
class PhysicalObject { double weight; }
class Car extends PhysicalObject {}
class Real {
private final double value;
Real(double r) {
this.value = r;
}
double getValue() {
return value;
}
}
class PositiveReal extends Real {
PositiveReal(double r) {
super(r);
assert(r > 0.0);
}
}
Comparator<Real> realComparator = (Real r1, Real r2) -> {
double v1 = r1.getValue();
double v2 = r2.getValue();
return v1 < v2 ? 1 : v1 > v2 ? -1 : 0;
};
Function<PhysicalObject, PositiveReal> weight = p -> new PositiveReal(p.weight);
// bad "weight"-function that cannot guarantee that the outputs
// are positive
Function<PhysicalObject, Real> surrealWeight = p -> new Real(p.weight);
// bad weight function that works only on cars
// Note: the implementation contains nothing car-specific,
// it would be the same for every other physical object!
// That means: code duplication!
Function<Car, PositiveReal> carWeight = p -> new PositiveReal(p.weight);
// Example 1
// badCompare1(weight, realComparator); // doesn't compile
//
// type error:
// required: Function<A,B>,Comparator<B>
// found: Function<PhysicalObject,PositiveReal>,Comparator<Real>
// Example 2.1
// Comparator<Car> c2 = badCompare2(weight, realComparator); // doesn't compile
//
// type error:
// required: Function<? super A,B>,Comparator<B>
// found: Function<PhysicalObject,PositiveReal>,Comparator<Real>
// Example 2.2
// This compiles, but for this to work, we had to loosen the output
// type of `weight` to a non-necessarily-positive real number
Comparator<Car> c2_2 = badCompare2(surrealWeight, realComparator);
// Example 3.1
// This doesn't compile, because `Car` is not *exactly* a `PhysicalObject`:
// Comparator<Car> c3_1 = badCompare3(weight, realComparator);
//
// incompatible types: inferred type does not conform to equality constraint(s)
// inferred: Car
// equality constraints(s): Car,PhysicalObject
// Example 3.2
// This works, but with a bad code-duplicated `carWeight` instead of `weight`
Comparator<Car> c3_2 = badCompare3(carWeight, realComparator);
// Example 4
// That's how it's supposed to work: compare cars by their weights. Done!
Comparator<Car> goodComparator = goodCompare(weight, realComparator);
}
}
Related links
- Detailed illustration of definition-site covariance and contravariance in Scala: How to check covariant and contravariant position of an element in the function?
Let's say, for example, we want to compare commercial flights by what plane they use. We would therefore need a method that takes in a flight, and returns a plane:
Plane func (CommercialFlight)
That is, of course, a Function<CommercialFlight, Plane>
.
Now, the important thing is that the function returns a Plane
. It doesn't matter what kind of plane is returned. So a method like this should also work:
CivilianPlane func (CommercialFlight)
Now technically this is a Function<CommercialFlight, CivilianPlane>
, which is not the same as a Function<CommercialFlight, Plane>. So without the
extends`, this function wouldn't be allowed.
Similarly, the other important thing is that is can accept a CommercialFlight
as an argument. So a method like this should also work:
Plane func (Flight)
Technically, this is a Function<Flight, Plane>
, which is also not the same as a Function<CommercialFlight, Plane>
. So without the super
, this function wouldn't be allowed either.