Before You Begin
As usual, pull the skeleton code.
Learning Goals
In this lab, we’ll wrap up the Java-focused portion of the class. You’ll learn about two fundamental parts of comparing objects, the .equals()
method for equality, and the Comparator
and .compareTo()
method for ordering. Both techniques will be widely used in the remainder of the class. We will then discuss inheritance and its rules, followed by higher-order functions.
Comparison
Read Chapter 4.3 from Max Function through Comparables to help motivate the problem we’re solving and the tools we’ll use along the way.
Casting is a bit of special syntax where you can tell the compiler that a specific expression has a specific compile-time type. If the
maxDog
method below returns an object of static typeDog
, the code normally wouldn’t compile asPoodle
is a subtype ofDog
. Casting tells Java to treat theDog
as if it were aPoodle
for the purposes of compilation because it’s possible that theDog
returned frommaxDog
could be aPoodle
.Poodle largerPoodle = (Poodle) maxDog(frank, frankJr);
You don’t need to fully understand how it works just yet for this first part of the lab.
While we haven’t explicitly learned about sorting yet, the idea of sorting should be intuitive enough. You have a list of things, and you want to put it in sorted order. While this makes sense immediately for things like int
s, which we can compare with primitive operations like <
and ==
, this becomes less clear for general objects.
Take a look at the Java API for the Comparable<T>
interface. A lot of things implement Comparable
in the Java standard library, as comparing two of the same type of object is a very common idea.
The only method required by Comparable
is compareTo(T o)
which takes another object of the (same) type T
and returns an int
whose value represents whether this
or o
should come first. In order to sort a list in Java, most sorting algorithms will call compareTo
and make pairwise comparisons to determine which object should come first, repeatedly, and swap elements until the entire list is sorted. (The hard part of sorting, then, is to determine which compareTo
‘questions’ are necessary to ask!)
Here are a few key details from compareTo
, slightly adapted:
Compares this object with the specified object for order. Returns a negative integer, zero, or a positive integer if this object is less than, equal to, or greater than the specified object, respectively.
There are other requirements that usually just happen naturally with a reasonable implementation, but are still important to specify:
The implementor must also ensure that the relation is transitive:
(x.compareTo(y) > 0 && y.compareTo(z) > 0)
impliesx.compareTo(z) > 0
.It is strongly recommended, but not strictly required that
x.compareTo(y) == 0
is equivalent tox.equals(y)
. Generally speaking, any class that implements theComparable
interface and violates this condition should clearly indicate this fact. The recommended language is “Note: this class has a natural ordering that is inconsistent with equals.”
Typically, a class will compare to objects of the same type as itself (although it does not strictly have to). Doing so means data structures that require ordering (like sorted lists, and in the future, search trees) can contain the class.
Exercise: User
In User.java
, we’ve provided an example of how a website might model a user.
Make User
implement Comparable
. Use the generic parameter to Comparable
to ensure that User
can only be used to compare against other User
s.
The natural ordering for User
is to compare by ID number. If their ID numbers are the same, then compare them on their username.
After implementing this, you should be able to sort User
s:
public static void main(String[] args) {
User[] users = {
new User(2, "Christine", ""),
new User(4, "Kevin", ""),
new User(5, "Alex", ""),
new User(1, "Lauren", ""),
new User(1, "Catherine", "")
};
Arrays.sort(users);
for (User user : users) {
System.out.println(user);
}
}
User{id=1, name=Catherine, email=}
User{id=1, name=Lauren, email=}
User{id=2, name=Christine, email=}
User{id=4, name=Kevin, email=}
User{id=5, name=Alex, email=}
Note that here we use Arrays.sort
because users
is an array; if it was a Java Collection
like ArrayList
, we would use Collections.sort
.
Discussion: Inheritance and Overriding
Read from the beginning of Chapter 4.2 through (and including) Casting.
There are lots of tricky concepts in this chapter; the best way to learn them is by applying them through practice and remember that Java follows a very strict and precise set of rules.
Between you and your partner, come up with a definition and procedure in your own words for each of the following terms.
- Compile-time type (also known as static type)
- Runtime type (also known as dynamic type)
- Superclass
- Subclass
- Extends
- Overriding and dynamic method selection
Discussion: Cats and Dogs
Discuss the first two problems from the Spring 2018 Exam Prep 4 worksheet and check your answers against the solution.
Make sure that both partners understand why each answer is what it is. A good question to really check your understanding is to ask, “What if we removed this method?” or, “What if we added another overloaded method?”
Refer to the Inheritance Rules below if you’re not sure how to handle a particular problem. These formal procedures are helpful to reference when working through some of the trickier situations that can happen when designing with inheritance.
Assessment
Now that we’ve learned a bit more about inheritance, complete the Gradescope assessment with your partner to check your understanding!
Recap: Inheritance Rules
All fields (class and instance variables) and static methods (class methods) are looked up against the static type of the variable or expression.
- Overloading
- Overloading occurs when there are multiple methods within a class with the same name, but different parameters. Note that, “In a subclass, you can overload the methods inherited from the superclass. Such overloaded methods neither hide nor override the superclass instance methods—they are new methods, unique to the subclass.”
- Dynamic Method Selection
- “An instance method in a subclass with the same signature (name, plus the number and the type of its parameters) and return type as an instance method in the superclass overrides the superclass’s method.”
Note that selecting methods by the dynamic type of the object only applies to non-static, instance methods! All other members, including static methods, and both static and non-static variables, are looked up against the static type of the object only: no overriding occurs for these members!
At compile-time, the compiler, using the static type, creates a link to the exact matching method signature, which includes the name, number and type of parameters (as they appear exactly), and return type of the method. This information is saved in the
.class
file generated byjavac
.Then, at runtime, if the method is an instance method, then override it with the method with a matching signature that is closest to the dynamic type of the object. An overriding method can also return a subtype of the type returned by the overridden method.
- Casting
- A type cast temporarily changes the static type of a variable, and tells the compiler to trust us, the programmer, that it will work.
So upcasting is always okay.
Dog d = new Dog(); Animal a1 = (Animal) d; // this is okay Animal a2 = d; // same thing
A cast fails at compile-time if the compiler knows, based on the static type of the variable being casted, that it cannot possibly succeed at runtime:
Tree tree = new Tree(); Animal a3 = (Animal) tree; // compile-time error
Since no
Tree
is ever anAnimal
, the compiler knows this cast cannot possibly succeed. So we get a compile-time error:incompatible types: Tree cannot be converted to Animal
.Here’s a downcast:
Animal a4 = new Dog(); Dog dd = (Dog) a4; // no error
a4
has dynamic typeDog
, anddd
is a reference to aDog
, so it’s okay fordd
to refer toa4
.But now consider:
Animal a5 = new Animal(); Dog ddd = (Dog) a5; // runtime error
At compile-time, the reasoning is, “
a5
has static typeAnimal
. Soa5
could have dynamic typeDog
, in which case, the cast will succeed at runtime. Since it’s possible that the cast will succeed, we will allow this to compile.” (Notice that the compiler does not know thata5
has dynamic typeAnimal
.)But then here’s what happens at runtime:
a5
has dynamic typeAnimal
, andddd
is a reference to aDog
. It’s not okay for a reference to aDog
to refer to anAnimal
. (Why not?) So we get a runtime error:ClassCastException: Animal cannot be cast to Dog
.This example is thanks to Allen Guo.
Nicole Rasquinha, a TA from last semester, created a video playlist reviewing and applying this material if you prefer a video presentation of the material. Since these videos are on the long side, it’s best to watch them at home. You might find them helpful when studying later.
The summary video provides a good overview of the compiler’s role in dynamic method selection.
- Summary
- The compiler is a cautious proofreader
- Compiler only knows static type
- Method Procedure
- During compile-time, look in static type for method
- Write down exact method signature including name and parameters
- During runtime, only override if method signature is the same including parameters
Some of the excerpts above were quoted from the Java Tutorials on Overriding and Hiding Methods. Note that this tutorial covers material such as static method hiding which you don’t need to know about.
Higher-Order Functions
Then, read about Higher-Order Functions, the topic we’ll be exploring and building upon for the rest of lab.
A higher-order function is a function that treats other functions as data. In the textbook, we saw that we could create higher-order functions with the help of inheritance by defining additional classes to represent each operation.
public interface IntUnaryFunction {
int apply(int x);
}
public class TenX implements IntUnaryFunction {
/* Returns ten times the argument. */
public int apply(int x) {
return 10 * x;
}
}
By defining the IntUnaryFunction
interface, the Java compiler is able to type check and ensure that all parts of the program are safe to run.
There are some significant downsides to this design. For every function you want to use, you have to define a new class, along with writing all super-interfaces for the whole hierarchy. One shortcut you can take is defining anonymous classes, a common pattern used in programming for the Android OS, but one we will not explore in this course. Instead, we’ll take a look at a few other recent Java features like lambdas, which reduce the amount of code we need to write by having the compiler do as much as it can to infer the relationship between TenX
and IntUnaryFunction
.
Comparators
A Comparator<T>
is an example of a Java higher-order function for imposing ordering on objects (if they have no natural ordering), and/or overriding their natural ordering as specified by Comparable
.
Read Comparator in Chapter 4.3 of the online textbook.
Comparator
only requires one method to be implemented: compare(T o1, T o2)
, which “Returns a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.” It should behave transitively & symmetrically, and returning zero should also imply that o1.equals(o2)
.
Then, we can pass in the Comparator
object as the second argument to Arrays.sort
(the same works for Collections.sort
) and sort on whatever we want. For example, if we wanted to sort integers in reverse order, we can create a ReverseComparator
and reverse the result usual Integer
comparison.
public class ReverseComparator implements Comparator<Integer> {
public Integer compare(Integer o1, Integer o2) {
return -(Integer.compare(o1, o2));
}
public static void main(String[] args) {
int[] arr = {1, 2, 3};
Arrays.sort(arr, new ReverseComparator());
// The above line sorts based on ReverseComparator's compare method
System.out.println(Arrays.toString(arr)); // [3, 2, 1]
}
}
Function
Interface
The Java standard library now includes a Function
interface, a standard interface for any programmer to pass around function objects. Instead of having to use our own made-up interface, we can implement interfaces from Java’s universal Function
hierarchy.
Now, when it comes to writing things like Comparators, it means we no longer have to write a custom class. This is because a Comparator
is a functional interface (that is, it only has one abstract method that must be implemented) and can be replaced by any appropriate lambda function or method reference.
Now we can use Arrays.sort
like a true higher order function. Let’s revisit the example of sorting integers in reverse order.
Here are two ways to do it.
- Using a method reference
-
public class IntSorter { public static int reverseCompare(Integer i1, Integer i2) { return -(Integer.compare(i1, i2)); } public static void main(String[] args) { int[] arr = {1, 2, 3}; Arrays.sort(arr, IntSorter::reverseCompare); System.out.println(Arrays.toString(arr)); // [3, 2, 1] } }
The
::
notation denotes thereverseCompare
method of theIntSorter
class. You can also do this for instance methods (without associating it with an object) or with the instance method of a particular object (likeinstance::method
). This is the equivalent of passing the method around by reference.Note some of the differences between this and the previous
Comparator
example. Here, we pass in a method reference.IntSorter
does not implement theComparator
interface, and we never make a new instance ofIntSorter
- Using a lambda expression
-
public static void main(String[] args) { Integer[] arr = {1, 2, 3}; Arrays.sort(arr, (o1, o2) -> -(Integer.compare(o1, o2))); System.out.println(Arrays.toString(arr)); // [3, 2, 1] }
Notice the syntax here. First, the arguments to the method are declared, then an arrow, followed by the return value. The typed parameters are not provided, and the compiler will infer them automatically by their use. Of course, longer and more specific lambda statements are possible, using multiple statements and return statements, and using typed parameters, like so:
Arrays.sort(arr, (Integer o1, Integer o2) -> { System.out.println("Do more stuff here"); return -(Integer.compare(o1, o2)); });
Exercise: getOrderedBy
Take a look at DBTable.java
. It is a simple thin wrapper around a List<T>
. You will be adding functionality for a few common operations on a list of things like what we find in databases. In our example, T
will always be a User, but your code should generalize to any type.
Implement getOrderedBy
.
public <R extends Comparable<R>> List<T> getOrderedBy(Function<T, R> getter)
The
<R extends Comparable<R>>
is a method generic, which declares a generic type local to a single method (rather than the entire class), and can be used throughout the method similar to generics in classes. The method generic goes after the visibility keyword,public
, and before the return type declaration,List<T>
. Additionally, there is a bound on the generic typeR
, stating that only types that extendComparable<R>
will be accepted by the compiler.
getOrderedBy
should take in a method reference to a getter method of the T
class, and return a new List<T>
, ordered on the return value of that getter method. Example usage:
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User(2, "Christine", ""),
new User(4, "Kevin", ""),
new User(5, "Alex", ""),
new User(1, "Lauren", ""),
new User(1, "Catherine", "")
);
DBTable<User> t = new DBTable<>(users);
List<User> l = t.getOrderedBy(User::getName);
l.forEach(System.out::println);
}
User{id=5, name=Alex, email=}
User{id=1, name=Catherine, email=}
User{id=2, name=Christine, email=}
User{id=4, name=Kevin, email=}
User{id=1, name=Lauren, email=}
Like before, the Function
interface only requires a single method be implemented, R apply(T t)
, which represents the behavior of the Function itself. Functions also have other methods, which are marked default
, that can do other cool features, like composing functions with the compose
method, similar to currying in CS 61A, and chaining functions through andThen
.
Implement getOrderedBy
according to specification. You will need to make usage of a custom comparator, without writing Comparator
once.
Hint: Use a lambda statement. Do not mutate DBTable’s entries
.
Recap
- A
max
Function - Suppose we want to write a function
max()
that returns the max of any array regardless of type. If we write a methodmax(Object[] items)
, where we use the ‘>’ operator to compare each element in the array, this will not work! Why is this the case?Well, this makes the assumption that all objects can be compared. But some objects cannot! Alternatively, we could write a
max()
function inside the Dog class, but now we have to write amax()
function for each class that we want to compare! Remember, our goal is to write a “one true max method” that works for all comparable objects. OurComparable
Interface- The solution is to create an interface that contains a
compareTo(Object)
method; let’s call this interfaceOurComparable
. Now, ourmax()
method can take aOurComparable[]
parameter, and since we guarantee that any object which extends the interface has all the methods inside the interface, we guarantee that we will always be able to call acompareTo
method, and that this method will correctly return some ordering of the objects.Now, we can specify a “one true max method”. Of course, any object that needs to be compared must implement the
compareTo
method. However, instead of re-implementing themax
logic in every class, we only need to implement the logic for picking the ordering of the objects, given two objects. Comparable
Interface- Java has an in-built
Comparable
interface that uses generics to avoid any weird casting issues. Plus,Comparable
already works for things likeInteger
,Character
, andString
; moreover, these objects have already implemented amax
,min
, etc. method for you. Thus you do not need to re-do work that’s already been done! - Overloading
- In Java, methods in a class can have the same name, but different parameters. For example, a
Math
class can have anadd(int a, int b)
method and anadd(float a, float b)
method as well. The Java compiler is smart enough to choose the correct method depending on the parameters that you pass in. Methods with the same name but different parameters are said to be overloaded. - Overriding
- For each method in
AList
that we also defined inList
, we will add an @Override right above the method signature. As an example:@Override public Item get(int i) { ... }
This is not necessary, but is good style and thus we will require it. Also, it allows us to check against typos. If we mistype our method name, the compiler will prevent our compilation if we have the @Override tag.
- Static vs. Dynamic Type
- Every variable in Java has a static type. This is the type specified when the variable is declared, and is checked at compile-time. Every variable also has a dynamic type; this type is specified when the variable is instantiated, and is checked at runtime. As an example:
Thing a; a = new Fox();
Here,
Thing
is the static type, andFox
is the dynamic type. This is fine because all foxes are things. We can also do:Animal b = a;
This is fine, because all foxes are animals too. We can do:
Fox c = b;
This is fine, because
b
points to aFox
. Finally, we can do:a = new Squid()
This is fine, because the static type of
a
is aThing
, andSquid
is a thing. - Dynamic Method Selection
- The rule is, if we have a static type
X
, and a dynamic typeY
, then ifY
overrides the method fromX
, then on runtime, we use the method inY
instead. Student often confuse overloading and overriding. - Overloading and Dynamic Method Selection
- Dynamic method selection plays no role when it comes to overloaded methods. Consider the following piece of code, where
Fox extends Animal
.Fox f = new Fox(); Animal a = f; define(f); define(a);
Let’s assume we have the following overloaded methods in the same class:
public static void define(Fox f) { ... } public static void define(Animal a) { ... }
Line 3 will execute
define(Fox f)
, while line 4 will executedefine(Animal a)
. Dynamic method selection only applies when we have overridden methods. There is no overriding here, and therefore dynamic method selection does not apply. Comparator
Interface- The term “Natural Order” is used to refer to the ordering implied by a
Comparable
’scompareTo
method. However, what if we want to order ourDog
objects by something other thansize
? We will instead pass in aComparator<T>
interface, which demands acompare()
method. We can then implement thecompare()
method anyway we want to achieve our ordering.
Deliverables
Here’s a quick recap of what you need to do to complete this lab!
- Understand how
compareTo
relates to higher order functions. - Understand how method overloading and overriding work with inheritance relationships.
- Make the
User
class implementComparable
. - Implement
getOrderedBy
inDBTable.java
.
Complete the Gradescope assessment and make sure to submit the coding exercises in User.java
and DBTable.java
.