Comparison and Bounds
3.4 Comparator
Sometimes we want to compare objects that do not implement the Comparable interface, or to compare objects using a different ordering from the one specified by that interface.
The ordering provided by the Comparable interface is called the natural ordering, so the Comparator interface provides, so to speak, an unnatural ordering.
We specify additional orderings using the Comparator interface, which contains two methods:
interface Comparator<T> { public int compare(T o1, T o2);
public boolean equals(Object obj);
}
The compare method returns a value that is negative, zero, or positive depending upon whether the first object is less than, equal to, or greater than the second object—just as with compareTo. (The equals method is the one familiar from class Object; it is included in the interface to remind implementors that equal comparators must have compare methods that impose the same ordering.)
Example 3-1. Permitting comparison of apples with oranges abstract class Fruit implements Comparable<Fruit> { protected String name;
protected int size;
protected Fruit(String name, int size) { this.name = name; this.size = size;
}
public boolean equals(Object o) { if (o instanceof Fruit) { Fruit that = (Fruit)o;
3.4 Comparator | 37
return this.name.equals(that.name) && this.size == that.size;
} else return false;
}
public int hashCode() {
return name.hashCode()*29 + size;
}
public int compareTo(Fruit that) { return this.size < that.size ? - 1 : this.size == that.size ? 0 : 1 ; }
}
class Apple extends Fruit {
public Apple(int size) { super("Apple", size); } }
class Orange extends Fruit {
public Orange(int size) { super("Orange", size); } }
class Test {
public static void main(String[] args) {
Apple a1 = new Apple(1); Apple a2 = new Apple(2);
Orange o3 = new Orange(3); Orange o4 = new Orange(4);
List<Apple> apples = Arrays.asList(a1,a2);
assert Collections.max(apples).equals(a2);
List<Orange> oranges = Arrays.asList(o3,o4);
assert Collections.max(oranges).equals(o4);
List<Fruit> mixed = Arrays.<Fruit>asList(a1,o3);
assert Collections.max(mixed).equals(o3); // ok }
}
Example 3-2. Prohibiting comparison of apples with oranges abstract class Fruit {
protected String name;
protected int size;
protected Fruit(String name, int size) { this.name = name; this.size = size;
}
public boolean equals(Object o) { if (o instanceof Fruit) { Fruit that = (Fruit)o;
return this.name.equals(that.name) && this.size == that.size;
} else return false;
}
public int hashCode() {
return name.hashCode()*29 + size;
}
protected int compareTo(Fruit that) { return this.size < that.size ? -1 : this.size == that.size ? 0 : 1 ; }
class Apple extends Fruit implements Comparable<Apple> { public Apple(int size) { super("Apple", size); }
public int compareTo(Apple a) { return super.compareTo(a); } }
class Orange extends Fruit implements Comparable<Orange> { public Orange(int size) { super("Orange", size); }
public int compareTo(Orange o) { return super.compareTo(o); } }
class Test {
public static void main(String[] args) {
Apple a1 = new Apple(1); Apple a2 = new Apple(2);
Orange o3 = new Orange(3); Orange o4 = new Orange(4);
List<Apple> apples = Arrays.asList(a1,a2);
assert Collections.max(apples).equals(a2);
List<Orange> oranges = Arrays.asList(o3,o4);
assert Collections.max(oranges).equals(o4);
List<Fruit> mixed = Arrays.<Fruit>asList(a1,o3);
assert Collections.max(mixed).equals(o3); // compile-time error }
}
Here is a comparator that considers the shorter of two strings to be smaller. Only if two strings have the same length are they compared using the natural (alphabetic) ordering.
Comparator<String> sizeOrder = new Comparator<String>() {
public int compare(String s1, String s2) { return
s1.length() < s2.length() ? -1 : s1.length() > s2.length() ? 1 : s1.compareTo(s2) ;
} };
Here is an example:
assert "two".compareTo("three") > 0;
assert sizeOrder.compare("two","three") < 0;
In the natural alphabetic ordering, "two" is greater than "three", whereas in the size ordering it is smaller.
The Java libraries always provide a choice between Comparable and Comparator. For every generic method with a type variable bounded by Comparable, there is another generic method with an additional argument of type Comparator. For instance, corre- sponding to:
public static <T extends Comparable<? super T>>
T max(Collection<? extends T> coll)
we also have:
3.4 Comparator | 39
public static <T>
T max(Collection<? extends T> coll, Comparator<? super T> cmp)
There are similar methods to find the minimum. For example, here is how to find the maximum and minimum of a list using the natural ordering and using the size ordering:
Collection<String> strings = Arrays.asList("from","aaa","to","zzz");
assert max(strings).equals("zzz");
assert min(strings).equals("aaa");
assert max(strings,sizeOrder).equals("from");
assert min(strings,sizeOrder).equals("to");
The string "from" is the maximum using the size ordering because it is longest, and
"to" is minimum because it is shortest.
Here is the code for a version of max using comparators:
public static <T>
T max(Collection<? extends T> coll, Comparator<? super T> cmp) {
T candidate = coll.iterator().next();
for (T elt : coll) {
if (cmp.compare(candidate, elt) < 0) { candidate = elt; } }
return candidate;
}
Compared to the previous version, the only change is that where before we wrote candidate.compareTo(elt), now we write cmp.compare(candidate,elt). (For easy refer- ence, this code and what follows is summarized in Example 3-3.)
It is easy to define a comparator that provides the natural ordering:
public static <T extends Comparable<? super T>>
Comparator<T> naturalOrder() {
return new Comparator<T> {
public int compare(T o1, T o2) { return o1.compareTo(o2); } }
}
Using this, it is easy to define the version of max that uses the natural ordering in terms of the version that uses a given comparator:
public static <T extends Comparable<? super T>>
T max(Collection<? extends T> coll) {
return max(coll, Comparators.<T>naturalOrder());
}
A type parameter must be explicitly supplied for the invocation of the generic method naturalOrder, since the algorithm that infers types would fail to work out the correct type otherwise.
It is also easy to define a method that takes a comparator and returns a new comparator with the reverse of the given ordering:
public static <T> Comparator<T>
reverseOrder(final Comparator<T> cmp) {
return new Comparator<T>() {
public int compare(T o1, T o2) { return cmp.compare(o2,o1); } };
}
This simply reverses the order of arguments to the comparator. (By the contract for comparators, it would be equivalent to leave the arguments in the original order but negate the result.) And here is a method that returns the reverse of the natural ordering:
public static <T extends Comparable<? super T>>
Comparator<T> reverseOrder() {
return new Comparator<T>() {
public int compare(T o1, T o2) { return o2.compareTo(o1); } };
}
Similar methods are provided in java.util.Collections, see Section 17.4.
Finally, we can define the two versions of min in terms of the two versions of max by using the two versions of reverseOrder:
public static <T>
T min(Collection<? extends T> coll, Comparator<? super T> cmp) {
return max(coll, reverseOrder(cmp));
}
public static <T extends Comparable<? super T>>
T min(Collection<? extends T> coll) {
return max(coll, Comparators.<T>reverseOrder());
}
(This ends the code summarized in Example 3-3.)
The Collections Framework does provide two versions each of min and max with the signatures given here, see Section 17.1. However, if you examine the source code of the library, you will see that none of the four is defined in terms of any of the others; instead, each is defined directly. The more direct version is longer and harder to maintain, but faster. With Sun’s current JVM, measurements show a speedup of around 30 percent.
Whether such a speedup is worth the code duplication depends on the situation in which the code is used. Since the Java utilities might well be used in a critical inner loop, the designers of the library were right to prefer speed of execution over economy of expression. But this is not always the case. An improvement of 30 percent may sound impressive, but it’s insignificant unless the total time of the program is large and the routine appears in a heavily-used inner loop. Don’t make your own code needlessly prolix just to eke out a small improvement.
As a final example of comparators, here is a method that takes a comparator on elements and returns a comparator on lists of elements:
3.4 Comparator | 41
public static <E>
Comparator<List<E>> listComparator(final Comparator<? super E> comp) { return new Comparator<List<E>>() {
public int compare(List<E> list1, List<E> list2) { int n1 = list1.size();
int n2 = list2.size();
for (int i = 0; i < Math.min(n1,n2); i++) { int k = comp.compare(list1.get(i), list2.get(i));
if (k != 0) return k;
}
return (n1 < n2) ? -1 : (n1 == n2) ? 0 : 1;
} };
}
The loop compares corresponding elements of the two lists, and terminates when cor- responding elements are found that are not equal (in which case, the list with the smaller element is considered smaller) or when the end of either list is reached (in which case, the shorter list is considered smaller). This is the usual ordering for lists; if we convert a string to a list of characters, it gives the usual ordering on strings.