about 4 years ago

今天離開Lambda相關的議題,看一下Java 8另一個和語言相關的改變:default methods。過去,Java的介面只能宣告函式卻不能提供實作(或是說是定義函式,定義函式和宣告函式的差別就是有沒有實作),因此大多數的library設計都將工具函式(static utility methods)放在類別中,這基本上沒什麼大問題,但是對有些潔癖的人來說,就會考慮要不要把該類別的建構子設為private?

另外,Java不支援多重繼承,但能實作多個介面,所以在Java的函式庫裡常會看到一種設計慣例:介面宣告需要的函式,透過抽象類別提供共用的預設實作,最後由預設類別提供完整的實作,例如:TableModel宣告JTable所需要的最小函式集合,AbstractTableModel提供部分共用的實作(listeners的管理),最後DefaultTableModel提供一個完整的實作。這樣的慣例引起另一個問題:但是當A類別繼承了B類別,但又想使用C介面的抽象類別實作就沒辦法了(因為不能多重繼承),A類別只能自己實作C介面,某種程度上算是一種duplicated code。

自己在設計Comic Surfer的過程中,為了讓既有的外掛和主程式能維持binary compatibility,也採用上述的慣例,在公開的外掛SDK裡,同時放了介面和抽象類別,不過在開發文件上鼓勵開發者以繼承抽象類別的方式開發外掛,這樣的設計是當我想在介面中新增函式的宣告,同時在抽象類別中為新增的函式提供預設的實作,如此,即使外掛沒有用新的SDK進行修改,還是能在新版的Comic Surfer中掛載使用,而不會讓JVM抱怨說某個類別沒有提供完整的實作(不相容)。

上述的問題,在這次的default methods得到了一個不錯的解法。首先,工具函式不需要再放在類別中了,如Code List 1所示,靜態函式的實作能直接放在介面中。

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;
    }
}

接著,抽象類別某種程度上可以被取代(如果抽象類別帶有成員變數就沒辦法),假設要替溫度感測器設計一個介面,可以像Code List 2的TemperatureSensor介面,並提供二個default methods:getTemperatureInCelsius()getTemperatureInFahrenheit()分別將絕對溫度轉成常用的攝氏溫度和華氏溫度(這個例子是一個簡化的示意。不算是一個好設計,因為這轉換應該算是能抽離到外部變成一個獨立類別)。這時過去需要提供類似Code List 3中的抽象類別AbstractTemperatureSensor就完全不需要了,因為實作已經放在介面裡了。

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;
    }
}

不需要抽象類別,提供了一個設計上的彈性,例如要替一個溫度感測裝置控制器(AbstractDeviceControl)寫Adapter時,在沒有default methods前,因為單一繼承,要思考是讓Adapter繼承AbstractDeviceControl比較好,還是繼承AbstractTemperatureSensor比較好?當然,用composition解決更好,只是有時候會多寫些程式。現在可以像Code List 4用透過實作介面的方式取得預設實作,然後將唯一的繼承機會讓給其他可能需要的設計。

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);
    }
}

然後,如果要替介面新增函式,也可以透過直接在介面中提供預設實作,提供向下相容性。例如Code List 5新增蘭金溫度表示法(Rankine scale)到TemperatureSensor中,並提供預設的實作,這時TemperatureSensorDevice不需做任何修改就能夠使用。

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;
    }
}

既然是介面,當然能用擴充(extend)的方式產生新介面,例如,某些國家習慣用華氏溫度,所以很多感測器都只提供華氏溫度,這時可像Code List 6,擴充TemperatureSensor介面產生新介面FahrenheitTemperatureSensor,在擴充的時候,可以用abstract關鍵字將一個有預設實作的getTemperatureInFahrenheit()重新宣告為抽象函式,或是複寫既有的實作,例如複寫getTemperatureInCelsius()變成直接取華氏溫度轉攝氏溫度,不用多轉一次絕對溫度然後再轉攝氏溫度。

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;
    }
}

其實,default methods似乎也引起不小的討論,因為default methods加上可以實作多個介面,已經有點像C++的多重繼承了,只差在沒辦法繼承成員變數而已,是好是壞就看怎麼使用了。我個人覺得還蠻方便的,如果不是因為default methods要到Java 8才有,我倒是考慮將Comic Surfer的外掛介面和工具函式類別用default methods改寫。

← Quick Glance of Java 8 - Default Methods Quick Glance of Java 8 - Default Methods →