| Previous | Next | |
| Rational_ex14 | Rational_ex15 | Sparkjava |
Rational: ex15
Sorting: Comparable, Comparators
Part of a series of tutorial articles about a Rational class.
Code examples referred to on this page can be found here: https://github.com/UCSB-CS56-pconrad/cs56-rational-ex15
Review ex11 about reading from files
You may want to review the material in tutorial rational_ex11 about reading from files before continuing, especially if you skipped over it previously.
In this exercise, we are going to read a number of Rationals from a file (just as we did in rational_ex11, but then we are going to sort them a few different ways.
Don’t write your own sorts!
The java.util package gives us several ways to sort things in very flexible ways without having to write our own sorts.
But to work with it, we’ll need to understand the java.lang.Comparable<T> interface, and the notion of a java.util.Comparator<T>.
These powerful concepts allow us to sort lists of items in multiple different ways e.g. sorting Student objects by name and perm, or MenuItems by name or category or price.
tl;dr
Here is the short version for folks who think the rest of this is too long, and therefore don’t read it (tl;dr)
java.lang.Comparable<T>is an interface that objects (e.g.Rational,Student,MenuItem) directly implement themselves- The method implemented is an instance method,
public int compareTo(T o) - Implement
Comparable<T>when you want to usejava.util.Collections.sort(someList);accoring to its “natural ordering”
- The method implemented is an instance method,
java.util.Comparator<T>is a@FunctionalInterfacefor a stand-alone object that provides acomparefunction- The method implemented is
public int compare(T o1, T o2) - It’s an instance method, but here, it’s an instance of the
Comparatoritself. - This allows us to specify these objects with lambda expressions.
- Implement and instantiate a
Comparator<T>class (either explictly, or implicitly using a lambda) when you want to sort using the ` sort(Comparator<? super E> c)method ofArrayList`
- The method implemented is
What are the different ways we could sort rational numbers?
Suppose we have a set of rational numbers in no particular order:
[1/3, -2/3, 4/5, 7/2, 8/9, 2, 8, 1/9, -8/9, 3/7]
One way to sort them is simply to sort them their place on the number line, using the traditional less-than and greater-then operations.
[-8/9, -2/3, 1/9, 1/3, 3/7, 4/5, 8/9, 2, 7/2, 8]
Here are those same numbers as decimals, so that you can easily see that they are in the usual numerical order:
[-0.889,-0.667,0.111,0.333,0.429,0.800,0.889,2.000,3.500,8.000]
Another way might be to put them in order first by their denominator, then by their numerator, like this.
[2, 8, 7/2, -2/3, 1/3, 4/5, 3/7, -8/9, 1/9, 8/9]
First, java.lang.Comparable<T>
It want to be able to sort objects of class T, and there is some normal, natural way to sort those objects, i.e. a single “obvious* way that folks might want to sort those things, then it is helpful for class T to implement the java.lang.Comparable<T> interface.
The benefit is that we can then just use the method java.util.Collections.sort, which can be called on any ArrayList<T> as long as T implements the java.util.Comparable<T> interface.
For example, if Rational were to implement java.lang.Comparable<Rational> in a way that compares them based on their place on the number line of real numbers (i.e. the set that in Math classes we call ℝ or for that matter, the set of rationals ℚ), then we could write:
ArrayList<Rational> nums = ReadFileOfRationals.readArrayListFromFile("rationals.txt");
java.util.Collections.sort(nums);
And the references in the ArrayList<Rational> referred to by nums would be sorted so that they point to Rational objects arranged in ascending order.
(As a reminder, if a class is in package java.lang, we can just drop the java.lang. The compiler works with Java code as if there were an implied import java.lang.* at the top of every Java source file. So we’ll just call this Comparable<T> and drop the java.lang from here on out. And you don’t need to import anything to refer to `Comparable
As with any interface, for a class T to implement Comparable<T>, it has to implement each and every abstract method of Comparable<T>.
Fortunately, it is easy to get a class to implement Comparable<T>. All you have to do is implement (i.e. @Override the method:
public int compareTo(T o)
which “compares this object with the specified object for order.”
The exact language of the Javadoc is:
Compares this object with the specified object for order. Returns a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.
To put it another way: the contract of compareTo is that it returns an integer that is:
- less than 0 if the object referred to by
thisis less than the object referred to byo - equal to 0 if
this.equals(o)(see note below) - greater than 0 if the object referred to by
thisis greater than the object referred to byo
(Note: Strictly speaking, there is some flexibility in the contract around returning a value equal to 0 as compared to the behavior of .equals, but let’s not get into that. Making them inconsistent is not recommended anyway, and we shouldn’t have any need for that.)
Since the Rational class already has methods lt and gt, we can implement the public int compareTo(T o) method quite easily. But, first, of course, we should write some tests for it.
@Test
public void test_compareTo_1_3_lt_2_3() {
assertTrue(new Rational(1,3).compareTo(new Rational(2,3)) < 0);
}
@Test
public void test_compareTo_2_3_eq_2_3() {
assertTrue(new Rational(2,3).compareTo(new Rational(2,3)) == 0);
}
@Test
public void test_compareTo_2_3_gt_1_3() {
assertTrue(new Rational(2,3).compareTo(new Rational(1,3)) > 0);
}
We then write the code by first changing the first line of Rational.java thus:
public class Rational implements Comparable<Rational> {
Note: Comparable<Rational>, not Comparable<T>. <T> is just a placeholder. If you want to be able to compare Rational objects, then you implement Comparable
We then add an implementation of the method, using our existing .gt and .equals methods:
@Override
public int compareTo(Rational o) {
if (this.equals(o))
return 0;
if (this.gt(o))
return 1;
else
return -1;
}
What does this allow us to do? It allows us to go into the main of ReadFileOfRationals and demonstrate how to
sort using the “natural ordering” provided by compareTo. Here’s what that code looks like:
java.util.Collections.sort(numbers);
And to run it, we can first do mvn package to create a jar file:
pconrad$ mvn package
...
(Lots of output from Maven not shown)
...
pconrad$ ls target/*.jar
target/rational11-1.0-SNAPSHOT.jar
pconrad$
Then, we can use a command of this form
- java -cp jarfile name-of-class arguments
Here, -cp stands for classpath and what follows is the name of a jar file that contains our classes. If we have multiple such jar files, we can separate those files with colons, e.g. -cp file1.jar:file2.jar
Here’s how we use a command of that form to run the main class from edu.ucsb.cs56.pconrad.rational.ReadFileOfRationals to read rational numbers from the file resources/rational1.txt:
pconrad$ java -cp target/rational11-1.0-SNAPSHOT.jar edu.ucsb.cs56.pconrad.rational.ReadFileOfRationals resources/rational1.txt
numbers (before sorting) = [2/3, 5/8, 9/10, -1/4]
numbers (after sorting) = [-1/4, 5/8, 2/3, 9/10]
pconrad$
As you can see, we have sorted the array of Rationals!
Now: java.util.Comparator<T>
The interface java.util.Comparator<T> is a @FunctionalInterface; i.e. an interface for a method with one and only one abstract method.
We can implement that interface by implementing this method, which “compares its two arguments for order”:
public int compare(T o1, T o2)
A few points:
- The
intvalue returned is very similar to that of thecompareTomethod ofComparable<T> - But, this is not an instance method of T.
- So instead of comparing
thistoo, we are comparingo1too2as follows (strictly speaking, in each case, the objects “referred to by o1 and o2”)- less than 0 if
o1is less thano2 - equal to 0 if
o1.equals(o2) - greater than 0 if
o1is greater thano2
- less than 0 if
So, the simplest Comparator we might implement for Rational is this one:
public class SimpleRationalComparator implements java.util.Comparator<Rational> {
@Override
public int compareTo(Rational r1, Rational r2) {
return r1.compareTo(r2);
}
}
But note that since java.util.Comparator<T> is a functional interface, we can both declare the class an instantiate it
more easily using, you guessed it, lambda expressions:
public static final java.util.Comparator<Rational> simpleComparator = (r1,r2) -> r1.compareTo(r2);
We can use Comparators to customize the sort order of various sorts. The program SortRationalsWithLambdas contains several examples of this.
When we compile the code to produce a jar with mvn package,
we can run this program as we did earlier, with java -cp... showing several ways of sorting.
java -cp target/rational11-1.0-SNAPSHOT.jar edu.ucsb.cs56.pconrad.rational.SortRationalsWithLambdas
Before sorting =
[1/3, -2/3, 4/5, 7/2, 8/9, 2, 8, 1/9, -8/9, 3/7]
as decimals =
[0.333,-0.667,0.800,3.500,0.889,2.000,8.000,0.111,-0.889,0.429]
After sort with Collections.sort(nums) =
[-8/9, -2/3, 1/9, 1/3, 3/7, 4/5, 8/9, 2, 7/2, 8]
as decimals =
[-0.889,-0.667,0.111,0.333,0.429,0.800,0.889,2.000,3.500,8.000]
After nums.sort(denomThenNum) =
[2, 8, 7/2, -2/3, 1/3, 4/5, 3/7, -8/9, 1/9, 8/9]
as decimals =
[2.000,8.000,3.500,-0.667,0.333,0.800,0.429,-0.889,0.111,0.889]
After nums.sort((r1,r2)->r1.compareTo(r2)) =
[-8/9, -2/3, 1/9, 1/3, 3/7, 4/5, 8/9, 2, 7/2, 8]
as decimals =
[-0.889,-0.667,0.111,0.333,0.429,0.800,0.889,2.000,3.500,8.000]
pconrad$
Here’s the code for each way of sorting. First we sort with Collections.sort(nums) which does not use a Comparator, but rather uses the fact that Rational implements Comparable.
But, if that way of sorting isn’t what we want—or if we are sorting items from a class that doesn’t implement Comparable, we can can create a Comparator, like this one:
/**
Comparator that orders first by denominator, then by numerator, e.g.
<code>1, 2, 3, -5/2, 1/2, 3/2, 5/2, -7/3, 1/3, 2/3, 4/3</code>.
The comparator is initialized with a Lambda Expression
*/
public static final Comparator<Rational> denomThenNum = (r1,r2) -> {
int r1num = r1.getNumerator();
int r2num = r2.getNumerator();
int r1denom = r1.getDenominator();
int r2denom = r2.getDenominator();
if (r1denom==r2denom) {
return r1num - r2num;
} else {
return r1denom - r2denom;
}
};
And then sort with it like this. Here, we are using the sort method of java.util.ArrayList itself.
nums.sort(denomThenNum);
The method we are using here is this one, and instance method (i.e. non-static method) of ArrayList<E>, which “Sorts this list according to the order induced by the specified Comparator”.
void sort(Comparator<? super E> c)
As you learned from the HFJ textbook, the syntax <? super E> simply means that the Comparator sorts things of type E, or some super class of type E. Note that E is considered a superclass of itself.
Another example is this one, which shows declaring a trivial comparator “inline”:
nums.sort( (r1, r2)->r1.compareTo(r2));
Because of the way Comparable<T> and Comparator<T> work, you should be able to see that this does exactly the same thing as this:
java.util.Collections.sort(nums);
If you can understand the reason that is true, you’ve got a pretty good grasp on this topic. If you don’t see why that’s true, go back and read through this tutorial, plus the relevant sections in HFJ again, and/or ask your instructional staff some questions on Piazza or during office hours.
For more examples:
- Some sample code (without explanations) https://github.com/UCSB-CS56-pconrad/java-sorting-and-comparators
| Previous | Next | |
| Rational_ex14 | Rational_ex15 | Sparkjava |