天天看點

Java8新特性之接口的預設方法和靜态方法

本系列文章翻譯自@shekhargulati的java8-the-missing-tutorial

我們都知道應該面向接口程式設計。接口給定使用者應該使用的協定,而不用依賴該接口的具體實作細節。

是以,為了做到松耦合,設計出幹淨的接口成為API設計的要素之一。SOLID五大原則之一的接口隔離原則要求我們設計有具體目的的小接口,而不是一個通用卻臃腫的接口。對你的類庫和應用來說,接口設計是能否得到幹淨而高效的API的關鍵。

這一節的代碼在ch01包中

如果你曾經設計過API,随着時間的推移,你可能覺得有必要在API中添加新的方法。一旦API已經釋出,想要在不破壞已有實作的前提下對一個接口添加方法會變得非常困難。為了說清這一點,假設你正在建構一個簡單的能夠支援

操作的電腦API。我們可以寫一個

Calculator

的接口,如下所示。為了簡單起見,我們将數值類型設為

int

public interface Calculator {

    int add(int first, int second);

    int subtract(int first, int second);

    int divide(int number, int divisor);

    int multiply(int first, int second);
}
           

回到這個

Caculator

接口,你建立了一個

BasicCaculator

的實作類,如下所示。

public class BasicCalculator implements Calculator {

    @Override
    public int add(int first, int second) {
        return first + second;
    }

    @Override
    public int subtract(int first, int second) {
        return first - second;
    }

    @Override
    public int divide(int number, int divisor) {
        if (divisor == ) {
            throw new IllegalArgumentException("divisor can't be zero.");
        }
        return number / divisor;
    }

    @Override
    public int multiply(int first, int second) {
        return first * second;
    }
}
           

靜态工廠方法

假設這個電腦API非常有用也容易上手。使用者隻需要自己建立一個

BasicCalculator

的執行個體,就能夠使用該API。你可以看到如下的代碼。

Calculator calculator = new BasicCalculator();
int sum = calculator.add(, );

BasicCalculator cal = new BasicCalculator();
int difference = cal.subtract(, );
           

然而有些使用者沒有面向接口

Caculator

進行程式設計,相反,他們面向該接口的 實作類

BasicCalculator

進行程式設計。你的API沒有強制使用者面向接口程式設計,因為

BasicCalculator

是public的。如果你将

BasicCalculator

聲明為protected,那麼你需要建立一個能夠提供

Calculator

實作類的靜态工廠。我們通過優化代碼來解決這個問題。

首先,我們将

BasicCalculator

聲明為protected以便使用者不能夠直接使用該類。

class BasicCalculator implements Calculator {
  // rest remains same
}
           

接着,我們編寫一個能夠給我提供

Calculator

執行個體的工廠類,如下所示。

public abstract class CalculatorFactory {

    public static Calculator getInstance() {
        return new BasicCalculator();
    }
}
           

現在,使用者會被迫面向

Calculator

接口進行程式設計,而且他們不能夠知道該接口具體的實作細節。

雖然我們實作了我們的目的,但我們添加的新類

CaculatorFactory

也增加了API的複雜度。現在API的使用者在使用API之前需要多了解一個類。這是在該問題在Java8之前唯一的解決方案。

Java8允許在接口中定義靜态方法。這允許API設計者在接口中定義像

getInstance

一樣的靜态工具方法,這樣就能夠使得API簡潔而精練。在接口中的靜态方法能夠用來代替輔助類(像

CalculatorFactory

),通常我們建立這些輔助類來定義一些與特定類型相關的輔助方法。舉例來說,

Collections

類就是一個定義了衆多輔助方法來與集合和其相關接口進行協作的輔助類。在

Collections

中定義的方法能夠輕易的添加到

Collection

接口或者它的子接口中。

以上的代碼在Java8中可以通過添加在

Calculator

接口中添加一個

getInstance

方法來改進。

public interface Calculator {

    static Calculator getInstance() {
        return new BasicCalculator();
    }

    int add(int first, int second);

    int subtract(int first, int second);

    int divide(int number, int divisor);

    int multiply(int first, int second);

}
           

與時俱進地優化API

有些使用者可能決定通過添加像

remainder

這樣的方法,或者為

Calculator

接口給出他們自己的實作來擴充

Calculator

API。通過與使用者的溝通後,你發現大多數使用者想要在

Calculator

接口中添加一個

remainder

方法。這看起來是一個非常簡單的API變動,是以你在API中多添加了一個方法。

public interface Calculator {

    static Calculator getInstance() {
        return new BasicCalculator();
    }

    int add(int first, int second);

    int subtract(int first, int second);

    int divide(int number, int divisor);

    int multiply(int first, int second);

    int remainder(int number, int divisor); // new method added to API
}
           

在接口中添加方法破壞了API的源碼相容性。這意味着實作了

Calculator

接口的使用者需要添加

remainder

方法的實作,否則他們的代碼将不能通過編譯。這對于API開發者來說是一個大問題,這使得API很難進行改進。在Java8之前,接口中是不能有方法的具體實作的。這往往在API需要拓展的時候成為一個問題,比如在接口定義中添加一兩個方法。

為了使API随着時間的推移能夠不斷改進,Java8允許使用者在接口中給方法提供預設實作。這些方法被稱為default方法,或者defender方法。實作了該接口的類不需要給這些方法提供實作。如果一個實作類為這些方法提供了實作,那麼新給出的實作将會被使用,否則接口中的預設實作将被使用。

List

接口定義了一些

default

方法,像

replaceAll

sort

splitIterator

default void replaceAll(UnaryOperator<E> operator) {
    Objects.requireNonNull(operator);
    final ListIterator<E> li = this.listIterator();
    while (li.hasNext()) {
        li.set(operator.apply(li.next()));
    }
}
           

如下面的代碼所示,我們可以通過添加一個

default

方法來解決上述的API問題。

default

方法通常使用已經存在的方法進行定義,如

remainder

方法使用了

subtract

multiply

divide

方法。

default int remainder(int number, int divisor) {
    return subtract(number, multiply(divisor, divide(number, divisor)));
}
           

多重繼承

Java中一個類隻能繼承一個類,但可以實作多個接口。現在在接口中包含方法的實作是可行的,Java也就有了類似多重繼承的特性。Java在類型層面已經存在多重繼承特性,現在在行為層面也有了多重繼承特性。有三條規則來幫助決定哪一個方法會被選中。

規則1:在類中定義的方法勝過在接口中定義的方法

interface A {
    default void doSth(){
        System.out.println("inside A");
    }
}
class App implements A{

    @Override
    public void doSth() {
        System.out.println("inside App");
    }

    public static void main(String[] args) {
        new App().doSth();
    }
}
           

這将列印出

inside App

,因為在實作類中已經覆寫了接口中定義的方法。

規則2:明确的接口勝過上層接口

interface A {
    default void doSth() {
        System.out.println("inside A");
    }
}
interface B {}
interface C extends A {
    default void doSth() {
        System.out.println("inside C");
    }
}
class App implements C, B, A {

    public static void main(String[] args) {
        new App().doSth();
    }
}
           

這将列印出

inside C

規則3:執行在類中明确指明的實作方式

interface A {
    default void doSth() {
        System.out.println("inside A");
    }
}
interface B {
    default void doSth() {
        System.out.println("inside B");
    }
}
class App implements B, A {

    @Override
    public void doSth() {
        B.super.doSth();
    }

    public static void main(String[] args) {
      new App().doSth();
    }
}
           

這将會列印出

inside B