over 4 years ago

Today, the topic is not about the Lambda expression -- is about the other change to the language in Java 8: default methods. In the past, the Java interface can only declare methods, but can not provide implementation (or called define methods). Therefore, in most design, static utility methods are defined in a class. Basically, that is not a big problem. However, someone may think about that should I define a private default constructor for the class?

In addition, Java does not support multiple inheritance, but supports multiple interface implementations. Thus, in the Java library, there is a commonly seen pattern: an interface that declares the required methods, an abstract class that provides the common implementation, and a default class that provides a completed implementation. For example, TableModel declares the minimum method set required by JTable, AbstractTableModel provides some common implementation (e.g., the management of listeners), and DefaultTableModel provides a completed implementation. This pattern may give rise to a problem: if class A has inherited class B, and wants to use (inherit) the abstract class of the interface C to reuse the implementation of C. However, Java does not support multiple inheritance, the only way is that A implements C by itself, but, to a certain extent, that will produce duplicated code.

During the design of Comic Surfer, in order to keep the binary compatibility between the main program and the plug-ins, I used the pattern mentioned above. In the public plug-in SDK, both interfaces and abstract classes are provided. However, in the public developer guide, I encourage the developers to inherit abstract classes to develop the plug-in. In that design, when I want to add methods into existing interfaces, I also add default implementation of new added methods into the abstract classes. Therefore, the plug-in can still run with the new version of Comic Surfer without any revising -- JVM will not complain that the class does not provide the required implementation of some methods.

For the above issues, the default methods can provide a pretty good solution. First, it is no need to define the utility methods in a class anymore. As shown in Code List 1, the implementation of static methods can be defined in an interface directly.

Code List 1 - Static utility methods in an interface
package java8.defaults;

import java.util.Collection;
import java.util.function.Predicate;

public interface CollectionUtils {

    public static <T> T find(Collection<T> collection, Predicate<T> predicate) {
        for(T item : collection) {
            if(predicate.test(item)) {
                return item;
            }
        }
        return null;
    }
}

In addition, to a certain extent, if no member data is inside an abstract class, the role of abstract classes can be replaced. Assume that an interface is required for temperature sensors. The TemperatureSensor interface can be desinged like Code List 2: two default methods getTemperatureInCelsius() and getTemperatureInFahrenheit() are provided to convert the Kelvin scale to Celsius scale and Fahrenheit scale, respectively. I knew that the design example is not good enough because the conversion can be extracted into independent classes, however, the simplification is appropriate to show the usage of default methods. Since the implementation has be defined in the interface, the abstract class AbstractTemperatureSensor as shown in Code List 3 is not needed anymore.

Code List 2 - The default methods in an interface
package java8.defaults;

public interface TemperatureSensor {

    static final double KELVIN_TO_FAHRENHEIT_FACTOR = 1.8;
    static final double KELVIN_TO_FAHRENHEIT_OFFSET = 459.67;
    static final double KELVIN_TO_CELSIUS_OFFSET = 273.15;

    public double getTemperatureInKelvin();

    default public double getTemperatureInCelsius() {
        return getTemperatureInKelvin() - KELVIN_TO_CELSIUS_OFFSET;
    }

    default public double getTemperatureInFahrenheit() {
        return getTemperatureInKelvin() * KELVIN_TO_FAHRENHEIT_FACTOR - KELVIN_TO_FAHRENHEIT_OFFSET;
    }
}
Code List 3 - The replaced abstract class
package java8.defaults;

public abstract class AbstractTemperatureSensor implements TemperatureSensor {

    private static final double KELVIN_TO_FAHRENHEIT_FACTOR = 1.8;
    private static final double KELVIN_TO_FAHRENHEIT_OFFSET = 459.67;
    private static final double KELVIN_TO_CELSIUS_OFFSET = 273.15;

    @Override
    public double getTemperatureInCelsius() {
        return getTemperatureInKelvin() - KELVIN_TO_CELSIUS_OFFSET;
    }

    @Override
    public double getTemperatureInFahrenheit() {
        return getTemperatureInKelvin() * KELVIN_TO_FAHRENHEIT_FACTOR - KELVIN_TO_FAHRENHEIT_OFFSET;
    }
}

No need of the abstract class provides some flexibility in design. For example, trying to write an adapter for a real temperature sensor device (AbstractDeviceControl), if default methods are not supported, due to the single inheritance, the developer needs consider that let the adapter inherit whether AbstractDeviceControl or AbstractTemperatureSensor? Of course, composition over inheritance, but, sometimes, developers need to write more codes. Now, just like Code List 4, the adapter can obtain the implementation from the interface, and the only one chance to inherit a class is left to other designs.

Code List 4 - Get the default implementation from interfaces
package java8.defaults;

public class TemperatureSensorDevice extends AbstractDeviceControl implements TemperatureSensor {

    private static final int COMMAND_CONTROL_PORT = 0x11;
    private static final int TEMPERATURE_OUTPUT_PORT = 0x10;
    private static final int TEMPERATURE_COMMAND = 0x0101;

    @Override
    public double getTemperatureInKelvin() {
        openCommandControlPort(COMMAND_CONTROL_PORT);
        setCommandOutputPort(TEMPERATURE_OUTPUT_PORT);
        performCommand(TEMPERATURE_COMMAND);
        return getValueFromPort(TEMPERATURE_OUTPUT_PORT);
    }
}

Then, while adding new methods into an existing interface, the default methods in the interface can provide the backward compatibility. For example, in Code List 5, a conversion to a new scale, called Rankine scale, is added into the TemperatureSensor interface with the default implementation. And, TemperatureSensorDevice can be used directly without any modification.

Code List 5 - Add a new method into TemperatureSensor with default implementation
package java8.defaults;

public interface TemperatureSensor {

    static final double KELVIN_TO_FAHRENHEIT_FACTOR = 1.8;
    static final double KELVIN_TO_FAHRENHEIT_OFFSET = 459.67;
    static final double KELVIN_TO_CELSIUS_OFFSET = 273.15;

    public double getTemperatureInKelvin();

    default public double getTemperatureInCelsius() {
        return getTemperatureInKelvin() - KELVIN_TO_CELSIUS_OFFSET;
    }

    default public double getTemperatureInFahrenheit() {
        return getTemperatureInKelvin() * KELVIN_TO_FAHRENHEIT_FACTOR - KELVIN_TO_FAHRENHEIT_OFFSET;
    }

    default public double getTemperatureInRankine() {
        return getTemperatureInKelvin() * KELVIN_TO_FAHRENHEIT_FACTOR;
    }
}

An interface can be extended to a new interface. For example, in some countries, people are used to Fahrenheit scale and many devices only provide the value in Fahrenheit scale. For that, like Code List 6, a new interface FahrenheitTemperatureSensor extends the TemperatureSensor interface. In the extension, the abstract modifier is used to redeclare the getTemperatureInFahrenheit() method that has default implementation as abstract method. A default method can also be overridden, e.g., getTemperatureInCelsius() is overridden to convert the Celsius scale from the Fahrenheit scale directly, saving the conversion between Kelvin scale and Fahrenheit scale.

Code List 6 - Redeclare getTemperatureInFahrenheit() as an abstract method
package java8.defaults;

public interface FahrenheitTemperatureSensor extends TemperatureSensor {

    static final double FAHRENHEIT_TO_KELVIN_FACTOR = 5.0 / 9.0;
    static final double FAHRENHEIT_TO_CELSIUS_OFFSET = 32;

    abstract public double getTemperatureInFahrenheit();

    default public double getTemperatureInKelvin() {
        return (getTemperatureInFahrenheit() + KELVIN_TO_FAHRENHEIT_OFFSET) * FAHRENHEIT_TO_KELVIN_FACTOR; 
    }

    default public double getTemperatureInCelsius() {
        return (getTemperatureInFahrenheit() - FAHRENHEIT_TO_CELSIUS_OFFSET) * FAHRENHEIT_TO_KELVIN_FACTOR;
    }
}

In fact, many developers discussed a lot about default methods because with both default methods and multiple interface implementations can provide some kind of multiple inheritance like C++ -- except that the member data are not inherited. Good or bad? It depends on how to use. I think default methods helpful. If default methods are not only available in Java 8, I really would like to revise the plug-in interfaces and utility classes with default methods in my Comic Surfer.

← Java 8 初探 - Default Methods Java 8 初探 - Default Methods →