Default Methods#
We’ve been focusing on the abstract
methods of an interface, those methods that have no implementation. It is common that an interface can add beneficial behavior simply by leveraging the other abstract
instance methods it knows about. These additive instance methods are called default
methods.
Furthermore, an interface can provide static
utility methods that have access to other static methods or fields, but not to any of the instance methods because a static method does not have access to this
.
In other words, interfaces can have implementation!
Interfaces with Implementation#
An interface can include methods with implementation. For example:
public interface MyInterface {
// An instance method implied to be `abstract`
public int countStuff(List<Integer> list);
// An instance method with implementation
public default void defaultMethod() {
// an instance method that has access to `this`
this.countStuff(null);
}
// A static method
public static void utilityMethod() {
// no access to `this` and therefore cannot call countStuff
System.out.println("This is a static method.");
}
}
Default methods will leverage and extend the interface.
Comparator#
Let’s look at some of the default
and static
methods found in the Comparator
interface. There are actually quite a few of these methods. The reason there are so many is because there is a lot of overloading to provide better type safety and convenience. Below are just four methods in the interface.
public interface Comparator<T> {
// The only `public abstract` method in the interface
int compare(T o1, T o2);
// Allows one to easily reverse the order of a sort
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
// This allows **chaining** of methods so that Primary and Secondary
// (then Tertiary, then Quaternary, then Quinary, etc) sort ordering can be done.
default Comparator<T> thenComparing(Comparator<? super T> other) {
// This is a quick way to verify that `other` is not null.
// If `other` is null, then this will throw an exception.
// See Footnote #1.
Objects.requireNonNull(other);
return (Comparator<T> & Serializable) (c1, c2) -> {
int res = compare(c1, c2);
return (res != 0) ? res : other.compare(c1, c2);
};
}
// This allows us to sort objects by some property on the object.
// It makes use of a Key Extractor to identify the property to sort on.
public static <T, U> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor,
Comparator<? super U> keyComparator)
{
Objects.requireNonNull(keyExtractor);
Objects.requireNonNull(keyComparator);
return (Comparator<T> & Serializable)
(c1, c2) -> keyComparator.compare(keyExtractor.apply(c1),
keyExtractor.apply(c2));
}
}
The above code can be daunting. This is largely due to all the fancy generic typing. Let’s examing the code piece-by-piece.
reversed()#
This method simply wraps up the Collections.reverseOrder
method. It allows the developer to write shorter, more readable code. It returns another Comparator
interface.
In the code below, we create a Comparator in three different ways for illustration purposes only.
Using a
static
method on theComparator
interface.Using a
lambda expression
and theComparable.compareTo
method.Using a
Method Reference
that is a bit more advanced. (See last lesson)
1public static void sortRev(List<String> list) {
2 // Makes use of the static method on the Comparator interface
3 Comparator<String> natural = Comparator.naturalOrder();
4
5 // Creates a Comparator using a Lambda expression
6 natural = (s1, s2) -> s1.compareTo(s2);
7
8 // Creates a Comparator using a Method Reference where this syntax
9 // is using "an arbitrary instance" to transform an instance method
10 // that takes one argument, into a method that takes two arguments.
11 // There is more information on this in the last lesson of this chapter.
12 natural = String::compareTo;
13
14 // This makes use of the Comparator's default method to say that
15 // we want to reverse the natural order.
16 list.sort(natural.reversed());
17}
Details on creating the Comparator:
On line 3 we make use of the
Comparator
’sstatic
method that will generate a Natural OrderComparator
. Note that we invoke thestatic
method like we would any static method on a class. The implementation of this method is effectively a complicated version ofreturn c1.compareTo(c2);
.On line 6 we recreate the
Comparator
using a lambda expression that implements the onlyabstract
method inComparator
. It makes use of theComparable
interface. Note that thecompareTo
method is not invoked at this time. We are creating an interface with an anonymous method that will be invoked later when the list is sorted.On line 12 we once again recreate the
Comparator
using aMethod Reference
. This method reference appears to have erroneous syntax because it isclassName::instanceMethod
, something we have not yet seen. Early we showed that when thecontext::
is the class name, what follows is astaticMethod
. To understand this new syntax see the last lesson.
Finally, after creating the natural order Comparator
, we get to using the default
method reversed
. Since reversed
is an instance method, we dereference it from the identifier natural
and invoke it using parentheses. If you examine line 16 above you’ll see that natural.reversed()
returns a Comparator
that is passed into the sort
method. The result is a sort order that is reversed from its natural order.
thenComparing#
This default
method has a complicated prototype because it makes use of bounded wildcards
. Bounded wildcards make your code more flexible and reusable while maintaining type safety. Comparator<? super T> other
means the comparator can accept any type that is a superclass of T
. The ?
is the wildcard
and it means any type that can’t be known at compile time. The syntax allows you to pass any comparator that works on a type that is broader than T
. It makes the code more reusable. We will discuss this in more detail in the section Bounded Wildcards below.
The simplest way to understand these complicated prototypes is to erase the bounds. We will also erase the Intersection Type[2].
We can simplify to get the following code. While it is not as reusable, it is effectively the same and much easier to understand.
default Comparator<T> thenComparing(Comparator<T> other) {
return (c1, c2) -> {
// First, call the `compare` method on "this"
// to see how the two object compare.
int res = compare(c1, c2);
// Use a ternary statement to continue the comparison using
// the next comparison found in `other`. Continue only if "this"
// comparison results in equality.
return (res != 0) ? res : other.compare(c1, c2);
};
}
comparing#
The comparing
method in the Comparator
appears even more complex as it makes heavier use of Constraint Typing
and it introduces a new generic type U
. Let’s first write the simplied version of the method and then explain it.
public static <T, U> Comparator<T> comparing(
Function<T, U> keyExtractor,
Comparator<U> keyComparator) {
return (c1, c2) -> keyComparator.compare(keyExtractor.apply(c1),
keyExtractor.apply(c2));
}
Here is what we can now see a bit more clearly:
The method
comparing
returns aComparator
that compares two objects of typeT
. It doesn’t excute any code; it returns a function (interface).The method takes two arguments:
keyExtractor
: This is a function that will get something of typeU
from an object of typeT
. This object of typeU
is used to establish the order.keyComparator
: This is a function that compares two objects of typeU
. It is what we actually sort by.
Let’s do an example. Let’s say I want to compare my Kid
s by their age
in reverse order. I would need to extract their age
. I would call their age the key
. I get their age using the keyExtractor
function. Once I have this key value, I need a way to compare two keys to establish the sort order. As you can see, the method comparing
accepts a Comparator
that is capable of comparing objects of type U
which is the type of age
(Integer).
In the example code below, we provide two sorting methods that sort by age in reverse order. The first one uses the static
method comparing
on the Comparator
interface. We provide the keyExtractor
method using a lambda expression: it returns the integer age. Then we use another lambda expression to create a Comparator
that does the arithmetic to do a reverse ordering of integers.
There is a 3rd sorting method below that sorts by age in natural order.
public class Person {
public int age;
}
public class Kid extends Person { }
public class Example {
public void sort1(List<Kid> kids) {
kids.sort(Comparator.comparing(p -> p.age, (a1, a2) -> a2 - a1));
}
public void sort2(List<Kid> kids) {
kids.sort((k1, k2) -> k2.age - k1.age);
}
// Sort in the natural order. Provide only the keyExtractor
public void sort3(List<Kid> kids) {
kids.sort(Comparator.comparing(p -> p.age));
}
}
sort3
does not accept a keyComparator
and therefore uses the natural order of integers. We can do this because the Comparator
interface has an overloaded version of comparing
that accepts only the keyExtractor
. Below we show the original code and its simplified version that is easier to read.
// original version as found in the Java Libraries.
// Note that this has a complicated constraint that U must implement Comparable.
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
{
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
// Simplified version. This won't compile because we removed
// the constraint that U must implement Comparable.
public static <T, U> Comparator<T> comparing(Function<T, U> keyExtractor) {
return (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
Footnotes#
[1] The code Objects.requireNonNull
is not technically necessary. If an argument is null, the NullPointerException
will be thrown when the dereference is attempted. However, there are benefits to this explicit code:
Fail Fast Principle
It throws the exception immediately when the method is called, rather than later when the object is used. This makes debugging easier because the stack trace points directly to the source of the problem.Clear Intent
It explicitly documents that null is not allowed for that parameter. This improves code readability and helps other developers understand the contract of the method.
[2] Intersection Type: This is when we dual cast something to two interfaces simultaneously. This allows you to treat an object as implementing multiple interfaces at once. This is especially useful in generic programming, where you want to enforce that a type satisfies multiple constraints. In this lesson we have: (Comparator<T> & Serializable)
. This is a way to cast to BOTH interfaces. It says that the resulting “thing” IS-A both a Comparator
and Serializable
. Recall that an object can implement two interfaces. Here we are establishing that an implementation provides the behavior of both. This is done with the &
operator between two types.
Another Example
Suppose you have two interfaces:
interface Flyable {
void fly();
}
interface Trackable {
void track();
}
And a class that implements both:
class Drone implements Flyable, Trackable {
public void fly() {
System.out.println("Flying...");
}
public void track() {
System.out.println("Tracking...");
}
}
You can use an intersection type like this:
Flyable flyableDrone = new Drone();
Trackable trackedDrone = (Flyable & Trackable) flyableDrone;
trackedDrone.track(); // Now you can call both fly() and track()
Real-World Use Case
In Java’s functional programming (e.g., lambdas), you might want a lambda that is both a Function<T, R>
and Closeable
, or both a Runnable
and AutoCloseable
. You can use intersection types to enforce that.
[3] The Serializable
interface is a marker interface; it has no methods or fields. Its purpose is to mark a class as being capable of serialization — converting an object into a byte stream for storage or transmission. Client code would inquire whether an object is Serializable using instanceof
. It is it, then the client code is free to serialize the object using code like the following:
public static void demoSerialize(Object obj) {
if (obj instanceof Serializable) {
try (FileOutputStream fileOut = new FileOutputStream("object.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
out.writeObject(obj);
System.out.println("Serialized data is saved in object.ser");
} catch (IOException ex) {
ex.printStackTrace();
}
}
}