CMPS 144: Generic Comparator-based Array Sorting in Java

The following is a class containing a method for sorting an array of int's. For simplicity, we chose to model the method after the classic (but not very efficient) insertion sort algorithm.

public class ArraySorter {

   public void insertionSort(int[] a) {

      // loop invariant: a[0..i) is a permutation of its initial
      //   contents and its elements are in ascending order
      for (int i=0;  i != a.length;  i = i+1) {
         int itemToInsert = a[i];
         int j = i;
         while (j != 0  &&  itemToInsert < a[j-1]) {
            a[j] = a[j-1];  j = j-1;
         }
         a[j] = itemToInsert;
      }
   }
} 

In order to make the class more generally applicable, we would like to change the method so that its parameter is an array of type Object[]. (Recall that Object is the "root" class in Java, which is to say a variable of type Object can refer to an instance of any class. If we simply make the proper substitutions of Object for int, we get

public class ArraySorter {

   public void insertionSort(Object[] a) {

      for (int i=0;  i != a.length;  i = i+1) {
         Object itemToInsert = a[i];
         int j = i;
         while (j != 0  &&  itemToInsert < a[j-1]) {
            a[j] = a[j-1];  j = j-1;
         }
         a[j] = itemToInsert;
      }
   }
}

But the boolean expression shown in red is syntactically incorrect! Why? Because the relational operator < is being applied to two values referred to by variables of type Object, for which no such operator is defined. (Indeed, < is defined only for primitive data types such as int, double, and char.) In order to fix this, we must refrain from using < in our sorting method. For similar reasons, we cannot use any of the other relational operators (e.g., <=, >, >=) that provide information regarding the lesser/equals/greater relationship between two items. But how can we sort a collection of items without the ability to compare them against one another?! Indeed, it would seem that some kind of comparison operation must be at the heart of any sorting algorithm.1

To the rescue comes the (generic) java.util.Comparator interface, which looks something like this:

public interface Comparator<T> {

   /* Returns a negative value if left is less than right.
   ** Returns zero if left is equal to right.
   ** Returns a positive value if left is greater than right.
   */
   int compare(T left, T right);
}

Note that all methods declared in a Java interface are implicitly public, so that including the keyword public in a method header is redundant, and hence optional.

In what follows, we illustrate how an instance of a class that implements this interface (namely, ComparePersonByAge) can be used in sorting arrays containing instances of class Person, where these classes are as follows:

public class Person {

   private String name;
   private int age;

   public Person(String name, int age) 
      { this.name = name;  this.age = age; }

   public String nameOf() { return name; }
   public int ageOf() { return age; }
   public String toString() { 
      return "Name: " + name + "  Age: " + age;
   }
} 
import java.util.Comparator;

public class ComparePersonByAge implements Comparator<Person> {

   /* Returns a negative value if p1 is younger than p2.
   ** Returns zero if p1 and p2 are of the same age.
   ** Returns a positive value if p1 is older than p2.
   */
   public int compare(Person p1, Person p2)
      { return p1.ageOf() - p2.ageOf(); }
}

The heading of ComparePersonByAge indicates not only that it implements the Comparator interface, but, more specifically, that it does so with respect to the Person data type. Which means that it is obligated to include a compare() method having a signature consistent with that in Comparator, except with any occurrence of T (the generic type parameter of Comparator) replaced by Person. (The formal parameter names need not match, as you can tell.)

As you can tell from the body of its compare() method, any instance c of ComparePersonByAge2 has the property that, for (nonnull) Person references p1 and p2, the expression c.compare(p1,p2) has a negative, zero, or positive value in accord with whether p1 is younger than, the same age as, or older than, respectively, p2. In other words, this class interprets "less than" to mean "younger than".

Now, to employ the Comparator-based approach, we incorporate generics and comparators into our array-sorting class to become the following:

import java.util.Comparator;

public class ArraySorterViaComparator<T> {

  private Comparator<T> c;  // the resident Comparator!

  /* Makes the given Comparator be this object's 
  ** "resident comparator" (which will be used whenever 
  ** this object is called upon to sort an array).
  */
  public ArraySorterViaComparator(Comparator<T> comp) 
     { this.c = comp; }

  /* Sorts the given array (using the Insertion Sort algorithm), 
  ** using the resident comparator to compare array elements.
  */
  public void insertionSort(T[] a) {

     for (int i=0;  i != a.length;  i = i+1) {
        T itemToInsert = a[i];
        int j = i;
        while (j != 0  &&  lessThan(itemToInsert, a[j-1])) {
           a[j] = a[j-1];  j = j-1;
        }
        a[j] = itemToInsert;
     }
  }

  /* Reports whether left < right according to the 
  ** resident comparator.
  */
  private boolean lessThan(T left, T right)

     { return c.compare(left, right) < 0; }
}

In order to create a "sorter object" capable of sorting arrays of type T[], a client passes an object of type Comparator<T> to the constructor method (via its parameter), which is saved in the new object's instance variable c. The resulting object's insertionSort() method sorts an array of type T[] into an order consistent with the compare() method of c.

Here is a client program that, using the Comparator-based approach, sorts an array of type Person[] by age, from youngest to oldest:

import java.util.Comparator;

class PersonSortViaComparatorApp {

   public static main(String[] args) {

      // establish p as an array of Persons 
      Person[] p = { new Person("Smith, John", 29),
                     new Person("Jones, Mary", 15),
                     new Person("Kelly, Maria", 22),
                     new Person("Floyd, Bob", 72),
                     new Person("Rumpelstiltskin", 256),
                     new Person("Dijkstra, Ed", 66),
                     new Person("Godfrey, Gourdhead", 22),
                     new Person("Smith, John", 18);
                   };
  
      // establish comp as a comparator that is based upon a person's age
      Comparator<Person> comp = new ComparePersonByAge();

      // establish sorter as an object that can sort arrays based upon comp
      ArraySorterViaComparator<Person> sorter = new ArraySorterViaComparator<>(comp);

      // let sorter do the job of sorting p[]
      sorter.insertionSort(p);

      // display the sorted contents of p[] (using a for..each loop)
      for (Person person : p) {
         { System.out.println( person ); }

      // alternative code for displaying the array, using a "regular" for-loop:
      //for (int i=0; i != p.length; i = i+1)
      //   { System.out.println( p[i] ); }
   }
} 

Notice that, because the declared type of sorter is ArraySorterViaComparator<Person>, which includes the type argument Person, we can (optionally) omit that argument on the right-hand side of the assignment to sorter. In cases such as this, the Java compiler infers that the absent type argument is intended to be whatever type argument appeared in the variable's declaration.


Exercises

Each exercise asks you to develop a class that implements java.util.Comparator<Person> and thereby defines an ordering upon instances of the Person class. In order to test each such class, you should modify the PersonSortViaComparatorApp program (as seen above) to make use of an instance of that class to sort the elements of array p[].

1. Develop a class ComparePersonByName that is analogous to ComparePersonByAge, with the difference being that the ordering that it defines upon Person objects corresponds to "alphabetical order by name" (i.e., as names appear in a telephone book).

Hint: Make use of the compareTo() method in the String class.

2. Develop a class ComparePersonByAgeName analogous to ComparePersonByAge in that it orders Person objects according to age, but, in cases where two people are of the same age, it orders them alphabetically by name. In other words, it uses age as the first criterion in making a comparison and, when the ages are equal, it uses name as the second criterion.

Referring to the application program above, a comparison between Gourdhead Godfrey and Maria Kelly should result in the former being deemed to be "less than" the latter, due to their ages being the same but the former's name being the lesser of the two.

3. Develop a class ComparePersonByNameLength analogous to ComparePersonByName, but such that it uses name length as the first criterion in making a comparison ("shorter in length" implies "less than") and, when the lengths are the same, it uses alphabetical ordering.

Referring to the application program above, a comparison between John Smith and Mary Jones should result in the latter being deemed to be "less than" the former, due to their name lengths being the same but "Jones ..." being alphabetically less than "Smith...".


Footnotes

[1] Actually, there are algorithms for sorting particular kinds of data that are not based upon comparing the items to be sorted against one another. An example is Radix Sort for sorting integers. But "general purpose" sorting algorithms do perform such comparisons.

[2] Because ComparePersonByAge has no instance variables, any two instances of it are identical. The same can be said for the two versions of the ArraySorter class first introduced. In some sense, classes having this property are degenerate.