Functional Programming for Java Developers, Part 5 - JDK8 Default Methods



Let's get back to the real Java. Java provides syntax to define abstract data types and write code imperatively. It allows mutable variables and objects. Are previous articles for amusements only? What should we take from functional programming? Well...if some geniuses have written some efficient methods, such as map, filter and reduce, we can just program to an interface without caring details. For example, adding 1 to each list element can be as follow:

map(list(1, 2, 3, 4, 5), x -> x + 1);

Filtering a list of elements less than 3 is as follow:

filter(list(1, 2, 3, 4, 5), x -> x > 3);

Summing up a list of elements can be done as follow:

reduce(list(1, 2, 3, 4, 5), (sum, x) -> sum + x, 0);

Yes, almost every Java programmer knows the principle - Program to an interface, not an implementation. Those geniuses may implement map, filter and reduce imperatively. They may implement short-circuit, concurrent, or lazy logic for efficiency, but you don't have to care about that. You just have to think and use those methods functionally. Am I mapping a list to another list? Am I filtering a new list from original one? Am I reducing a list to get the final result? Am I dividing my problem into sub problems? Once you are thinking functionally, you'll know why Joel Spolsky said that...

...programming languages with first-class functions let you find more opportunities for abstraction.

You can think functionally even you write imperative code finally. Thinking functionally may always give you a new idea or direction when writing code. That's why Simon Peyton Jones mentioned that...

...the perspectives you learn over here in the purely functional world can inform and illuminate the mainstream.

Certainly, some geniuses are willing to provide methods commonly used in functional programming languages. The question here is that, where should those methods rest? For example, where should map, filter and reduce rest? We can remain these methods static and rest them in the Collections class. This makes them like functions, such as map, filter and reduce in Python. Python, however, is a multi-paradigm language. Remaining map, filter and reduce to be functions is natural in Python. But, it's not the Java style because Java's main paradigm is object-oriented programming. Making them static methods in the Collections class would relegate them to a sort of second-class status. In Java, we may like a coding style as follow:

List<String> names = ...;
names.filter(s -> s.length() < 3)
     .forEach(s -> out.println(s));

This is more expressive than the style like this:

forEach(filter(names, s -> s.length() < 3), s -> out.println(s));

Can we add a method filter to the List interface? If you're using JDK7 or previous versions, the answer is No! All client code implementing the List interface will break. Building a new Collections2 APIs is a choice, but the Collection APIs permeate many libraries all over the world. Replacing the Collection APIs would be a major task and keep developers from using the new Collections2 APIs once JDK8 is released.

JDK8 finally takes an evolutionary strategy of evolving the interface syntax. We can add default implementations, called as default methods, to an interface. This seems like cheating because only the language creator or related organizations can take this strategy. But, it's definitely a way to do defensive API evolution with Java interfaces.

One example of default methods is the forEach method defined in the Iterable interface.

package java.lang;

import java.util.Iterator;
import java.util.Objects;
import java.util.function.Consumer;

@FunctionalInterface
public interface Iterable<T> {
    Iterator<T> iterator();
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
}

Any implementation of Iterable just has to implement the iterator method, and then the client can use the forEach method directly. For example, you can write code as follow:

List<String> names = ...;
names.forEach(
    name -> out.println(name.toUpperCase())
);
     
You don't break implementations of Iterable because the forEach method has been implemented. Default methods make an interface like an abstract class with abstract methods. The difference is that you cannot use field members in default methods because an interface cannot define field members. As you seen above, you can use default methods to implement the Template Method pattern. For example, you may define a Comparable as follow:

public interface Comparable<T> {
    int compareTo(T that);
   
    default boolean lessThan(T that) {
        return compareTo(that) < 0;
    }
    default boolean lessOrEquals(T that) {
        return compareTo(that) <= 0;
    }
    default boolean greaterThan(T that) {
        return compareTo(that) > 0;
    }
    ...
}

A Ball class implementing the Comparable interface just has to implement the compareTo method.

public class Ball implements Comparable<Ball> {
    private int radius;
    ...
    public int compareTo(Ball that) {
        return this.radius - that.radius;
    }
}

Then, every Ball instance owns those default methods defined in the Comparable interface. Because a class can implement more than one interface, default methods make interface something like the Trait in Scala, or the Module in Ruby. You can define shared implementations in some interfaces. Once a class needs some shared operations, they can implement necessary interfaces, implement abstract methods, and mixin shared operations.

Of course, there're several details about default methods. You can look more in the article State of the Lambda v4. Let's back to our previous example. We may like a coding style as follow:

List<String> names = ...;
names.filter(s -> s.length() < 3)
     .forEach(s -> out.println(s));

But in JDK8, we have to write code as follow:

List<String> names = ...;
names.stream()
     .filter(s -> s.length() < 3)
     .forEach(s -> out.println(s));
    
Why do we need that stream method? That's what we will look in the next article.