Classes Fall Short#
This lesson is dedicated to showing that classes are pretty good at allowing us to customize the sort order of a list. However, classes are not good enough. Interfaces are needed to sort a generic list of objects that define their own sort order.
We will demonstrate this by sorting lists in various ways using classes exclusively. We will continually increase the demand of our code until we discover what classes cannot do, but interfaces can.
Overview#
The goals of this lesson is to demonstrate that classes are unable to generically sort a list where the objects define their own sort order.
As we accomplish this goal, we will also:
🔹 Learn how to pass a function (a behavior) to another method.
🔹 Sort a List in multiple, custom ways.
Sorting Kids#
Let’s start off by sorting my kids. Let’s say that we have the following code:
public class Person {
public int height; // in inches
public int age; // in years
}
public class Kid extends Person {
public int cuteness;
public double gpa;
public boolean whinesTooMuch() { ... }
public void beLazy() { ... }
public void pretendToStudy() { ... }
}
public class Dad extends Person {
private List<Kid> kids = new ArrayList<>();
/*
* returns true if kid at index i1 is less than kid at index i2
*/
private boolean kidLessThan(int i1, int i2) {
/* implementation not shown */
}
// Sort using the insertion sort algorithm
public void sortKids() {
for (int index = 1; index < kids.size(); index++) {
// slide the nth kid into the correct place
for (int n = index; n > 0 && kidLessThan(n, n-1); n--) {
// swap kid n with (n - 1)
Kid temp = kids.get(n);
kids.set(n, kids.get(n - 1));
kids.set(n - 1, temp);
}
}
}
}
This code allows Dad to sort the kids according to the comparison code found in kidLessThan
. Here are three different implementations of kidLessThan
:
private boolean kidLessThan(int i1, int i2) {
// compare by height
return kids.get(i1).height < kids.get(i2).height;
}
private boolean kidLessThan(int i1, int i2) {
// compare by age
return kids.get(i1).age < kids.get(i2).age;
}
private boolean kidLessThan(int i1, int i2) {
// compare by first by whining, secondarily by cuteness
if (kids.get(i1).whinesTooMuch() == kids.get(i2).whinesTooMuch()) {
// if they whine the same amount, use the cuteness
return kids.get(i1).cuteness < kids.get(i2).cuteness;
}
// With different whining, the lesser kid whines too much.
return kids.get(i1).whinesTooMuch();
}
We can choose to sort our kids in any way, BUT in only one way. We can’t
have three simultaneous implementations of kidLessThan
. How can we get around this
implementation?
Class Implements Comparator#
We can pass in an object that implements the method kidLessThan
. Let’s look at that code.
public abstract class CompareKids {
public boolean kidLessThan(Kid k1, Kid k2);
}
public class CompareByHeight {
public boolean kidLessThan(Kid k1, Kid k2) {
// compare by height
return k1.height < k2.height;
}
}
public class CompareByAge {
public boolean kidLessThan(Kid k1, Kid k2) {
// compare by age
return k1.age < k2.age;
}
}
public class Dad {
public void sortKids(CompareKids comparator) {
for (int index = 1; index < kids.size(); index++) {
// slide the nth kid into the correct place
for (int n = index; n > 0 && comparator.kidLessThan(kids.get(n), kids.get(n-1)); n--) {
// swap kid n with (n - 1)
Kid temp = kids.get(n);
kids.set(n, kids.get(n - 1));
kids.set(n - 1, temp);
}
}
}
public void exampleSortCall() {
sortKids(new CompareByAge());
}
}
In the above implementation we can see how we changed the sortKids
method. It now takes an abstract class CompareKids
as an argument. That class must implement the method kidLessThan
that is called to determine the sort order. Furthermore, since these classes are external to Dad
, we need to pass in the Kid
and not just the index to the Kid. We can have lots of classes that derive from CompareKids
and sort our kids in many different ways without having to recompile.
While the current implementation works, it can’t be generalized to work with non-Kids. It can only sort Kids.
Generalized Sorting with Classes#
To generalize the above code, we can use Generics
in the following way.
public abstract class CompareThem<T> {
public boolean lessThan(T item1, T item2);
}
public class CompareTheirAge extends CompareThem<Kid> {
public boolean lessThan(Kid k1, Kid k2) {
// compare by age
return k1.age < k2.age;
}
}
public static <T> void sortList(CompareThem<T> comparator, List<T> list) {
for (int index = 1; index < list.size(); index++) {
// slide the nth element into the correct place
for (int n = index; n > 0 && comparator.lessThan(list.get(n), list.get(n-1)); n--) {
// swap element at n with (n - 1)
T temp = list.get(n);
list.set(n, list.get(n - 1));
list.set(n - 1, temp);
}
}
}
public void exampleSort() {
sortList(new CompareTheirAge(), kids);
}
With the above implementation, we can sort any list using any comparator method. And we did it without using interfaces!!
Kids Sorting Themselves#
Let’s rewrite the sorting algorithm so that the kids can sort themselves. In other words, let’s have the lessThan
method belong to the Kid class. This is what we call Natural Sort Order
.
It’s not too hard to add the method to the Kid class. In the code below, we will also take the opportunity to introduce a more standard way of doing comparisons. Take note on the comparison method, compareTo
.
public class Kid extends Person {
public int cuteness;
public double gpa;
public boolean whinesTooMuch() { ... }
/*
* return any negative value if this kid is less than the other kid.
* return 0 if this kid == other kid.
* return any positive value if this kid is greather than the other kid.
*/
public int compareTo(Kid other) {
// compare by age
return this.age - other.age;
}
}
public class Dad {
public void sortKids() {
for (int index = 1; index < kids.size(); index++) {
// slide the nth kid into the correct place
// if compareTo is < 0, then kid(n) is less than kid(n-1)
for (int n = index; n > 0 && kids.get(n).compareTo(kids.get(n-1)) < 0; n--) {
// swap kid n with (n - 1)
Kid temp = kids.get(n);
kids.set(n, kids.get(n - 1));
kids.set(n - 1, temp);
}
}
}
}
Let’s examine this solution. Most apparent is that is solves the problem we set out to solve. What is less than ideal?
The solution is not generic. The sorting method requires that we sort Kid
objects. In our attempt to make the sort method generic
we will quickly run into trouble because not all classes have the method compareTo. The code won’t compile!
public static <T> void sortList(List<T> list) {
for (int index = 1; index < list.size(); index++) {
// slide the nth element into the correct place
// THIS WON'T COMPILE because not all classes have the method compareTo
for (int n = index; n > 0 && list.get(n).compareTo(list.get(n-1)) < 0; n--) {
// swap element at n with (n - 1)
T temp = list.get(n);
list.set(n, list.get(n - 1));
list.set(n - 1, temp);
}
}
}
We could attempt to get around this problem by casting the object to a Kid
class. But that eliminates the generic nature of the method: casting to Kid
would mean we could only sort lists of Kid
objects.
We could require that objects inherit from a base class that implements the method. Let’s see what that looks like.
// Note: we are creating this **class** to illustrate a point.
// There is an interface named Comparable<T> that we will get to later.
public abstract class Comparable<T> {
public abstract int compareTo(T other);
}
public class Kid extends Person, Comparable<Kid> {
public int compareTo(Kid other) {
// compare by age
return this.age - other.age;
}
/* Other code not shown */
}
// This fancy generic declaration means that the List can be of any type
// so long as it extends Comparable<MyClassName>. This allows us to
// have access to the compareTo method.
public static <T extends Comparable<T>> void sortList(List<T> list) {
for (int index = 1; index < list.size(); index++) {
// slide the nth element into the correct place
// All classes that extend Comparable<T> have the method compareTo. Whew!
for (int n = index; n > 0 && list.get(n).compareTo(list.get(n-1)) < 0; n--) {
// swap element at n with (n - 1)
T temp = list.get(n);
list.set(n, list.get(n - 1));
list.set(n - 1, temp);
}
}
}
Does the above code work? NO!! Why not? Because it is important for the Kid
class to extend Person
. And classes have the limitation that we can extend only one class. It is impossible to have Kid
extend Person
AND Comparable<T>
. We were so close to providing a generic sorting method that can sort Kids that sort themselves. But it just won’t work!
What is the solution? Interfaces!! (Yes, that was obvious.)
By using interfaces, the code works just fine.
// This is a standard interface provided in Java. No need to re-declare it!
public interface Comparable<T> {
int compareTo(T other);
}
public class Kid extends Person implements Comparable<Kid> {
public int compareTo(Kid other) {
// compare by age
return this.age - other.age;
}
/* Other code not shown */
}
/* Sort method remains unchanged */
What’s so important?
#
We showed we could do the following when using only classes:
🔹 Customize the sort order by passing in a class that implements a comparison method
🔹 Sort a generic list of objects while also customizing the sort order
🔹 Sort a list of Kids where the kids sort themselves
We showed that classes are UNABLE to sort a generic list of objects that sort themselves because classes can extend only one other class. But, by using interfaces, we can successfully sort a generic list of objects that sort themselves.
The code in this lesson showed:
By passing in a class/interface, a function is effectively passed in as an argument. The function was “wrapped” inside of the class/interface.
The Type of the argument that is a function is an
interface
.Comparable
is aninterface
that allows objects to define their own sort order.
While we can use classes to represent a function, we’ve seen how there are shortcomings to using classes. Therefore, from here on out, we will always represent a function with an interface
.
Future lessons will show even more benefits to using an interface
. Classes fall even further behind!