Understanding Object Ordering in Java with Comparable and Comparator
- Details
- Written by Nam Ha Minh
- Last Updated on 17 June 2019   |   Print Email
- Collections.sort(list): sorts a List collection.
- Arrays.sort(array): sorts an array.
Example #1: Sorting a list of String objects
List<String> names = Arrays.asList( "Tom", "Peter", "Alice", "Bob", "Sam", "Mary", "Jane", "Bill", "Tim", "Kevin"); System.out.println("Before sorting: " + names); Collections.sort(names); System.out.println("After sorting: " + names);Output:
Before sorting: [Tom, Peter, Alice, Bob, Sam, Mary, Jane, Bill, Tim, Kevin] After sorting: [Alice, Bill, Bob, Jane, Kevin, Mary, Peter, Sam, Tim, Tom]In this example, the list names is sorted by alphabetic order of String.
Example #2: Sorting a list of Integer objects
List<Integer> numbers = Arrays.asList(8, 2, 5, 1, 3, 4, 9, 6, 7, 10); System.out.println("Before sorting: " + numbers); Collections.sort(numbers); System.out.println("After sorting: " + numbers);Output:
Before sorting: [8, 2, 5, 1, 3, 4, 9, 6, 7, 10] After sorting: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]Here, the integer numbers in the list numbers are sorted by alphanumeric order.
Example #3: Sorting a list of Date objects
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); List<Date> birthdays = new ArrayList<>(); birthdays.add(dateFormat.parse("2016-01-20")); birthdays.add(dateFormat.parse("1998-12-03")); birthdays.add(dateFormat.parse("2009-07-15")); birthdays.add(dateFormat.parse("2012-04-30")); System.out.println("Before sorting: "); for (Date date : birthdays) { System.out.println(dateFormat.format(date)); } Collections.sort(birthdays); System.out.println("After sorting: "); for (Date date : birthdays) { System.out.println(dateFormat.format(date)); }Output:
Before sorting: 2016-01-20 1998-12-03 2009-07-15 2012-04-30 After sorting: 1998-12-03 2009-07-15 2012-04-30 2016-01-20
- The natural ordering of String objects is alphabetic order.
- The natural ordering of Integer objects is alphanumeric order.
- The natural ordering of Date objects is chronological order.
Let’s dive into the concept of natural ordering.1. Understanding Natural Ordering
Natural ordering is the default ordering of objects of a specific type when they are sorted in an array or a collection. The Java language provides the Comparable interface that allows us define the natural ordering of a class. This interface is declared as follows:public interface Comparable<T> { public int compareTo(T object); }As you can see, this interface is parameterized (generics) and it has a single method compareTo() that allows two objects of a same type to be compared with each other. The important point here is the value returned by this method: an integer number indicates the comparison result of two objects. Remember these rules:
- compare value = 0: two objects are equal.
- compare value > 0: the first object (the current object) is greater than the second one.
- compare value < 0: the first object is less than the second one.
/** * Employee.java * @author www.codejava.net */ public class Employee { String firstName; String lastName; Date joinDate; public Employee(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String toString() { return firstName + " " + lastName; } // getters and setters }Add some employees to a list collection like this:
List<Employee> listEmployees = new ArrayList<>(); Employee employee1 = new Employee("Tom", "Eagar"); Employee employee2 = new Employee("Tom", "Smith"); Employee employee3 = new Employee("Bill", "Joy"); Employee employee4 = new Employee("Bill", "Gates"); Employee employee5 = new Employee("Alice", "Wooden"); listEmployees.add(employee1); listEmployees.add(employee2); listEmployees.add(employee3); listEmployees.add(employee4); listEmployees.add(employee5);Try to sort this list:
Collections.sort(listEmployees);We will get an error at runtime: no suitable method found for sort(List<Emmployee>)…WHY?It’s because the Employee class doesn’t implement the Comparable interface so the sort() method cannot compare the objects.Now, let’s have the Employee class implements the Comparable interface, and we define the natural ordering is first name - last name, meaning the employees are sorted by first name first, then by last name. Here’s the updated version of the Employee class:
/** * Employee.java * Implementing Comparable * @author www.codejava.net */ public class Employee implements Comparable<Employee> { // fields... // constructors... // getters... // setters... // implement the natural comparison method: public int compareTo(Employee another) { int compareValue = this.firstName.compareTo(another.firstName); if (compareValue == 0) { return this.lastName.compareTo(another.lastName); } return compareValue; } }Look at how the compareTo() method is implemented here:
- First, we compare the first name by using the String’s compareTo() method. We can safely use this method of the built-in types in Java: String, Date, Integer, Long, etc.
- If two employees have same first name (compare value = 0), then we compare their last name. Finally the compare value is returned as per the contract of the Comparable interface.
System.out.println("Before sorting: " + listEmployees); Collections.sort(listEmployees); System.out.println("After sorting: " + listEmployees);Output:
Before sorting: [Tom Eagar, Tom Smith, Bill Joy, Bill Gates, Alice Wooden] After sorting: [Alice Wooden, Bill Gates, Bill Joy, Tom Eagar, Tom Smith]Awesome! It works perfectly as we expected: the employees are sorted by their first name, and then last name. Note #1:We cannot compare objects of different types, e.g. a String object cannot be compared with an Integer object. As the compareTo() method enforces this rule, we can only compare objects of the same type. If we add objects of different types to a collection and sort it, we will get ClassCastException. Note #2:If we want to reverse the natural ordering, simply swap the objects being compared in the compareTo() method. For example, the following implementation sorts employees by their first name into descending order:
public int compareTo(Employee another) { return another.firstName.compareTo(this.firstName); }In case we use a sorted collection i.e. TreeSet, we don’t have to use the Collections.sort() utility method, as a TreeSet sorts its elements by their natural ordering. The following example demonstrates how to use a TreeSet to sort Strings:
Set<String> setNames = new TreeSet<>(); setNames.addAll(Arrays.asList("Tom", "Peter", "Alice", "Bob", "Sam", "Mary", "Jane", "Bill", "Tim", "Kevin")); System.out.println(setNames);Output:
[Alice, Bill, Bob, Jane, Kevin, Mary, Peter, Sam, Tim, Tom]Similarly, we can sort the Employee objects using a TreeSet like this:
Set<Employee> setEmployees = new TreeSet<>(); Employee employee1 = new Employee("Tom", "Eagar"); Employee employee2 = new Employee("Tom", "Smith"); Employee employee3 = new Employee("Bill", "Joy"); Employee employee4 = new Employee("Bill", "Gates"); Employee employee5 = new Employee("Alice", "Wooden"); setEmployees.add(employee1); setEmployees.add(employee2); setEmployees.add(employee3); setEmployees.add(employee4); setEmployees.add(employee5); System.out.println(setEmployees);Output:
[Alice Wooden, Bill Gates, Bill Joy, Tom Eagar, Tom Smith]So far we have got understanding about the natural ordering of objects and how the Comparable interface defines the ordering.What if we want to sort objects in an order which differs from the natural ordering? For example, sort the employees list above by seniority (based on their join dates)?
2. Understanding Comparator
The Collections utility class provides a method for sorting a list using an external comparator:Collections.sort(list, comparator)
This overloaded version takes two parameters: a list collection and a comparator, which is any object that implements the Comparatorinterface. This interface declares this method:public interface Comparator<T> { public int compare(T obj1, T obj2); }Like the Comparable interface, this interface is also parameterized for any specific type. The compare() method is similar except it takes both the objects to be compared as arguments. The return value is also evaluated similarly .For example, the following class compares two Employee objects using the Comparator interface:
/** * EmployeeComparator.java * Implementing Comparator * @author www.codejava.net */ public class EmployeeComparator implements Comparator<Employee> { public int compare(Employee emp1, Employee emp2) { return emp1.getJoinDate().compareTo(emp2.getJoinDate()); } }In this comparator, we compare two Employee objects by their join dates. And update the Employee class like this (add an overloaded constructor and update the toString() method):
/** * Employee.java * Implementing Comparable * @author www.codejava.net */ public class Employee implements Comparable<Employee> { // fields... // getters & setters.... // constructor public Employee(String firstName, String lastName, Date joinDate) { this.firstName = firstName; this.lastName = lastName; this.joinDate = joinDate; } public String toString() { DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); return firstName + " " + lastName + " " + dateFormat.format(joinDate); } }And here’s the test code:
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); List<Employee> listEmployees = new ArrayList<>(); Employee employee1 = new Employee("Tom", "Eagar", dateFormat.parse("2007-12-03")); Employee employee2 = new Employee("Tom", "Smith", dateFormat.parse("2005-06-20")); Employee employee3 = new Employee("Bill", "Joy", dateFormat.parse("2009-01-31")); Employee employee4 = new Employee("Bill", "Gates", dateFormat.parse("2005-05-12")); Employee employee5 = new Employee("Alice", "Wooden", dateFormat.parse("2009-01-22")); listEmployees.add(employee1); listEmployees.add(employee2); listEmployees.add(employee3); listEmployees.add(employee4); listEmployees.add(employee5); System.out.println("Before sorting: "); System.out.println(listEmployees); Collections.sort(listEmployees, new EmployeeComparator()); System.out.println("After sorting: "); System.out.println(listEmployees); Collections.sort(listEmployees, (emp1, emp2) -> emp1.getJoinDate().compareTo(emp2.getJoinDate()));Output:
Before sorting: [Tom Eagar 2007-12-03, Tom Smith 2005-06-20, Bill Joy 2009-01-31, Bill Gates 2005-05-12, Alice Wooden 2009-01-22] After sorting: [Bill Gates 2005-05-12, Tom Smith 2005-06-20, Tom Eagar 2007-12-03, Alice Wooden 2009-01-22, Bill Joy 2009-01-31]Note #3:Since Java 8, we can use Lambda expressions to create a comparator more easily like this:
Collections.sort(listEmployees, (emp1, emp2) -> emp1.getJoinDate().compareTo(emp2.getJoinDate()));We can also pass a comparator when creating a new instance of a TreeSet like this:
Set<Employee> setEmployees = new TreeSet<>(new EmployeeComparator());Then the TreeSet will sort its elements according to the order defined by the specified comparator.Using a comparator is useful in the following scenarios:
- The class doesn’t have natural ordering (or we don’t have source code to update it).
- We want to sort objects in orders other than the natural ordering.
- We want to provide multiple ways for sorting the objects, e.g. one comparator for each sorting criteria.
3. The contract between natural ordering and equals
You know, the documentation of both Comparable and Comparator states that the natural ordering and the ordering specified by a comparator should be consistent with the equals() method of the class. Let’s say we have two objects obj1 and obj2 of class A, then:If obj1.compareTo(obj2) = 0 then obj1.equals(obj2) = true
If this contract is violated, we will get strange behavior when using sorted collections such as TreeSet and TreeMap.Let’s examine an example to understand why this constraint really matters. Come back to the example of sorting a list of Employee objects.We haven’t overridden the equals() method yet. Now, let’s override it for the Employee class:/** * Employee.java * Implementing Comparable and override equals() * @author www.codejava.net */ public class Employee implements Comparable<Employee> { // fields, constructors, getters and setters and toString()... public int compareTo(Employee another) { int compareValue = this.firstName.compareTo(another.firstName); if (compareValue == 0) { return this.lastName.compareTo(another.lastName); } return compareValue; } public boolean equals(Object obj) { if (obj instanceof Employee) { Employee another = (Employee) obj; if (this.firstName.equals(another.firstName) && this.lastName.equals(another.lastName)) { return true; } } return false; } }Currently, it is compatible with the compareTo() method which also compares first name and then last name.What if we need to change the compareTo() method for comparing two Employee objects by their seniority (join date) like this:
public int compareTo(Employee another) { return this.joinDate.compareTo(another.joinDate); }Let’s execute some test code to see the outcome:
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); Set<Employee2> setEmployees = new TreeSet<>(new EmployeeComparator2()); Employee2 employee1 = new Employee2("Tom", "Eagar", dateFormat.parse("2007-12-03")); Employee2 employee2 = new Employee2("Tom", "Smith", dateFormat.parse("2005-06-20")); Employee2 employee3 = new Employee2("Bill", "Joy", dateFormat.parse("2007-12-03")); Employee2 employee4 = new Employee2("Bill", "Gates", dateFormat.parse("2005-05-12")); Employee2 employee5 = new Employee2("Alice", "Wooden", dateFormat.parse("2005-06-20")); setEmployees.add(employee1); setEmployees.add(employee2); setEmployees.add(employee3); setEmployees.add(employee4); setEmployees.add(employee5); System.out.println(setEmployees);Note that the employee1and employe5 have same join date, so do the employee3 and employee4. Add all of these 5 objects to the set:
setEmployees.add(employee1); setEmployees.add(employee2); setEmployees.add(employee3); setEmployees.add(employee4); setEmployees.add(employee5);And print the set:
System.out.println(setEmployees);Can you guess the output? Here is it:
[Tom Smith 2005-06-20, Tom Eagar 2007-12-03, Bill Joy 2009-01-31]Ouch! Why are there only 3 employees in the set?It’s because the set compares the objects using the compareTo() method which considers two employees are equal if they have same join date, whereas the set does not allow duplicate elements, hence the employee4 and employee5 objects are not added to the set.Now, you understand the consequence if natural ordering and equals are not consistent, right? So is there any solution or workaround?Yes, there is.Suppose that we still want to keep the natural ordering based on join date, while keep compatible with the equals() method, here’s how we update the compareTo() method:
public int compareTo(Employee another) { int compareValue = this.joinDate.compareTo(another.joinDate); if (compareValue == 0) { compareValue = this.firstName.compareTo(another.firstName); if (compareValue == 0) { return this.lastName.compareTo(another.lastName); } return compareValue; } return compareValue; }That’s it! In this solution, we compare the Employee objects by their join dates first. If equal, continue comparing by their first names. And if equal, continue comparing their last names. This way we can keep the compareTo() method compatible with the equals() method.Run the test code again and observe the output:
[Tom Smith 2005-06-20, Alice Wooden 2007-12-03, Tom Eagar 2007-12-03, Bill Gates 2009-01-31, Bill Joy 2009-01-31]Perfect!The same problem and solution applies for a comparator.Finally, I recommend you to look at the documentation of the Comparable and Comparator to understand more about the contract we’ve discussed so far:
Other Java Collections Tutorials:
- Java Set Tutorial
- Java Map Tutorial
- Java List Tutorial and
- Java Queue Tutorial
- Understanding equals() and hashCode() in Java
- 18 Java Collections and Generics Best Practices
Comments
Thanks for sharing that nice info.
// TreeSet of Employee sorted based on first name followed by last name and DateOfJoining
Set employeeSet = new TreeSet(
Comparator.comparing(Employee::getFirstName)
.thenComparing(Employee::getLastName)
.thenComparing(Employee::getDoJ)
);