Programmers frequently need to sort elements from a database into a collection, array, or map. In Java, we can implement whatever sorting algorithm we want with any type. Using the Comparable
interface and compareTo()
method, we can sort using alphabetical order, String
length, reverse alphabetical order, or numbers. The Comparator
interface allows us to do the same but in a more flexible way.
Whatever we want to do, we just need to know how to implement the correct sort logic for the given interface and type.
Sorting a Java List with a custom object
For our example we’ll use the same POJO we’ve used for other Java Challengers so far. In this first example, we implement the Comparable interface in the Simpson
class, using Simpson
in the generic type:
class Simpson implements Comparable<Simpson> {
String name;
Simpson(String name) {
this.name = name;
}
@Override
public int compareTo(Simpson simpson) {
return this.name.compareTo(simpson.name);
}
}
public class SimpsonSorting {
public static void main(String... sortingWithList) {
List<SimpsonCharacter> simpsons = new ArrayList<>();
simpsons.add(new SimpsonCharacter("Homer "));
simpsons.add(new SimpsonCharacter("Marge "));
simpsons.add(new SimpsonCharacter("Bart "));
simpsons.add(new SimpsonCharacter("Lisa "));
Collections.sort(simpsons);
simpsons.stream().map(s -> s.name).forEach(System.out::print);
Collections.reverse(simpsons);
simpsons.stream().forEach(System.out::print);
}
}
Note that we’ve overridden the compareTo() method and passed in another Simpson
object. We’ve also overridden the toString()
method, just to make the example easier to read.
The compareTo() method
The compareTo()
method compares a given object or the current instance with a specified object to determine the order of objects. Here’s a quick look at how compareTo()
works:
If the comparison returns |
Then ... |
|
|
|
|
|
|
We can only use classes that are comparable with the sort()
method. If we try to pass a Simpson
that does not implement Comparable
, we will receive a compilation error.
The sort()
method uses polymorphism by passing any object that is Comparable
. Objects will then be sorted as expected.
The output from the previous code would be:
Bart Homer Lisa Marge
If we wanted to reverse the order, we could exchange the sort()
for a reverse()
; from:
Collections.sort(simpsons);
to:
Collections.reverse(simpsons);
Deploying the reverse()
method would change the previous output to:
Marge Lisa Homer Bart
Sorting a Java array
In Java, we can sort an array with any type we want as long as it implements the Comparable
interface. Here’s an example:
public class ArraySorting {
public static void main(String... moeTavern) {
int[] moesPints = new int[] {9, 8, 7, 6, 1};
Arrays.sort(moesPints);
Arrays.stream(moesPints).forEach(System.out::print);
Simpson[] simpsons = new Simpson[]{new Simpson("Lisa"), new Simpson("Homer")};
Arrays.sort(simpsons);
Arrays.stream(simpsons).forEach(System.out::println);
}
}
In the first sort()
invocation, the array is sorted to:
1 6 7 8 9
In the second sort()
invocation, it is sorted to:
Homer Lisa
Keep in mind that custom objects must implement Comparable
in order to be sorted, even as an array.
Can I sort objects without Comparable?
If the Simpson object wasn’t implementing Comparable
, a ClassCastException would be thrown. If you run this as a test, you will see something like the following output:
Error:(16, 20) java: no suitable method found for sort(java.util.List<com.javaworld.javachallengers.sortingcomparable.Simpson>)
method java.util.Collections.<T>sort(java.util.List<T>) is not applicable
(inference variable T has incompatible bounds
equality constraints: com.javaworld.javachallengers.sortingcomparable.Simpson
lower bounds: java.lang.Comparable<? super T>)
method java.util.Collections.<T>sort(java.util.List<T>,java.util.Comparator<? super T>) is not applicable
(cannot infer type-variable(s) T
(actual and formal argument lists differ in length))
This log may be confusing, but don’t worry. Just keep in mind that a ClassCastException
will be thrown for any sorted object that doesn’t implement the Comparable
interface.
Sorting a Map with TreeMap
The Java API includes many classes to assist with sorting, including TreeMap. In the example below, we use TreeMap
to sort keys into a Map
.
public class TreeMapExample {
public static void main(String... barney) {
Map<SimpsonCharacter, String> simpsonsCharacters = new TreeMap<>();
simpsonsCharacters.put(new SimpsonCharacter("Moe"), "shotgun");
simpsonsCharacters.put(new SimpsonCharacter("Lenny"), "Carl");
simpsonsCharacters.put(new SimpsonCharacter("Homer"), "television");
simpsonsCharacters.put(new SimpsonCharacter("Barney"), "beer");
System.out.println(simpsonsCharacters);
}
}
TreeMap
uses the compareTo()
method implemented by the Comparable
interface. Each element in the resulting Map
is sorted by its key. In this case, the output would be:
Barney=beer, Homer=television, Lenny=Carl, Moe=shotgun
Remember, though: if the object doesn’t implement Comparable
, a ClassCastException
will be thrown.
Sorting a Set with TreeSet
The Set
interface is responsible for storing unique values, but when we use the TreeSet implementation, inserted elements will be automatically sorted as we add them:
public class TreeSetExample {
public static void main(String... barney) {
Set<SimpsonCharacter> simpsonsCharacters = new TreeSet<>();
simpsonsCharacters.add(new SimpsonCharacter("Moe"));
simpsonsCharacters.add(new SimpsonCharacter("Lenny"));
simpsonsCharacters.add(new SimpsonCharacter("Homer"));
simpsonsCharacters.add(new SimpsonCharacter("Barney"));
System.out.println(simpsonsCharacters);
}
}
The output from this code is:
Barney, Homer, Lenny, Moe
Again, if we use an object that is not Comparable
, a ClassCastException
will be thrown.
Sorting with Comparator
What if we didn’t want to use the same compareTo()
method from the POJO class? Could we override the Comparable
method to use a different logic? Below is an example:
public class BadExampleOfComparable {
public static void main(String... args) {
List<SimpsonCharacter> characters = new ArrayList<>();
SimpsonCharacter homer = new SimpsonCharacter("Homer") {
@Override
public int compareTo(SimpsonCharacter simpson) {
return this.name.length() - (simpson.name.length());
}
};
SimpsonCharacter moe = new SimpsonCharacter("Moe") {
@Override
public int compareTo(SimpsonCharacter simpson) {
return this.name.length() - (simpson.name.length());
}
};
characters.add(homer);
characters.add(moe);
Collections.sort(characters);
System.out.println(characters);
}
}
As you can see, this code is complicated and includes a lot of repetition. We had to override the compareTo()
method twice for the same logic. If there were more elements we would have to replicate the logic for each object.
Fortunately, we have the Comparator interface, which lets us detach the compareTo()
logic from Java classes. Consider the same example above rewritten using Comparator
:
public class GoodExampleOfComparator {
public static void main(String... args) {
List<SimpsonCharacter> characters = new ArrayList<>();
SimpsonCharacter homer = new SimpsonCharacter("Homer");
SimpsonCharacter moe = new SimpsonCharacter("Moe");
characters.add(homer);
characters.add(moe);
Collections.sort(characters, (Comparator.<SimpsonCharacter>
comparingInt(character1 -> character1.name.length())
.thenComparingInt(character2 -> character2.name.length())));
System.out.println(characters);
}
}
These examples demonstrate the main difference between Comparable
and Comparator
.
Use Comparable
when there is a single, default comparison for your object. Use Comparator
when you need to work around an existing compareTo()
, or when you need to use specific logic in a more flexible way. Comparator
detaches the sorting logic from your object and contains the compareTo()
logic within your sort()
method.
Using Comparator with an anonymous inner class
In this next example, we use an anonymous inner class to compare the value of objects. An anonymous inner class, in this case, is any class that implements Comparator
. Using it means we are not bound to instantiating a named class implementing an interface; instead, we implement the compareTo()
method inside the anonymous inner class.
public class MarvelComparator {
public static void main(String... comparator) {
List<String> marvelHeroes = new ArrayList<>();
marvelHeroes.add("SpiderMan ");
marvelHeroes.add("Wolverine ");
marvelHeroes.add("Xavier ");
marvelHeroes.add("Cyclops ");
Collections.sort(marvelHeroes, new Comparator<String>() {
@Override
public int compare(String hero1, String hero2) {
return hero1.compareTo(hero2);
}
});
Collections.sort(marvelHeroes, (m1, m2) -> m1.compareTo(m2));
Collections.sort(marvelHeroes, Comparator.naturalOrder());
marvelHeroes.forEach(System.out::print);
}
}
Using Comparator with lambda expressions
Anonymous inner classes are verbose, which can cause problems in our code. In the Comparator
interface, we can use lambda expressions to simplify and make the code easier to read. For example, we could change this:
Collections.sort(marvel, new Comparator<String>() {
@Override
public int compare(String hero1, String hero2) {
return hero1.compareTo(hero2);
}
});
to this:
Collections.sort(marvel, (m1, m2) -> m1.compareTo(m2));
Less code and the same result!
The output of this code would be:
Cyclops SpiderMan Wolverine Xavier
We could make the code even simpler by changing this:
Collections.sort(marvel, (m1, m2) -> m1.compareTo(m2));
to this:
Collections.sort(marvel, Comparator.naturalOrder());
Are the core Java classes Comparable?
Many core Java classes and objects implement the Comparable
interface, which means we don’t have to implement the compareTo()
logic for those classes. Here are a few familiar examples:
String
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence { ...
Integer
public final class Integer extends Number implements Comparable<Integer> { …
Double
public final class Double extends Number implements Comparable<Double> {...
There are many others. I encourage you to explore the Java core classes to learn their important patterns and concepts.
Take the Comparable interface challenge!
Test what you’ve learned by figuring out the output of the following code. Remember, you’ll learn best if you solve this challenge for yourself just by studying it. Once you’ve reached an answer, you can check the answer below. You can also run your own tests to fully absorb the concepts.
public class SortComparableChallenge {
public static void main(String... doYourBest) {
Set<Simpson> set = new TreeSet<>();
set.add(new Simpson("Homer"));
set.add(new Simpson("Marge"));
set.add(new Simpson("Lisa"));
set.add(new Simpson("Bart"));
set.add(new Simpson("Maggie"));
List<Simpson> list = new ArrayList<>();
list.addAll(set);
Collections.reverse(list);
list.forEach(System.out::println);
}
static class Simpson implements Comparable<Simpson> {
String name;
public Simpson(String name) {
this.name = name;
}
public int compareTo(Simpson simpson) {
return simpson.name.compareTo(this.name);
}
public String toString() {
return this.name;
}
}
}
Which is the output of this code?
A) Bart
Homer
Lisa
Maggie
Marge
B) Maggie
Bart
Lisa
Marge
Homer
C) Marge
Maggie
Lisa
Homer
Bart
D) Indeterminate