Appresso Engineer Blog

アプレッソのエンジニアが書く技術ブログです。

Java 8 勉強会・再び 第1回 ~デフォルトメソッド~

こんにちは、アプレッソ開発部の陳です。

秋が終わり、本格的に冬に入って、今年も残り僅かとなりました。

春(3月)から始まった Java 8 勉強会ですが、すでに予定したテーマを半分以上消化したにもかかわらず、内容は全然共有できていない状況です…(汗)

ここでアプレッソ Advent Calendar 2015とさくさにまぎれて勢いに乗って、Java 8 勉強会の内容共有を再開することにしました。 せっかくの再開なので、勉強会の内容をできるだけ細かく説明していきたいと思います。

今回はデフォルトメソッドについてお話ししたいと思います。

デフォルトメソッドのおさらい

Java 8 で導入された デフォルトメソッド は名前の通り、インタフェースで定義されたメソッドのデフォルト実装を提供する仕組みです。

基本的な使い方として二通りがあって、処理の一般形として提供するケース

// java.util.List (OpenJDK8)
default void sort(Comparator<? super E> c) {
    Collections.sort(this, c);
}

と、仕様の契約として提供するケースがあります。

// java.util.Iterator
default void remove() {
    throws new UnsupportedOperationException(”remove”);
}

デフォルトメソッドを導入しする主な意図は、API拡張時の既存実装とのソース互換性を維持するためです。 上記の例にあったように、実際 Java 8 では新しいフィーチャーの互換性を維持するため、デフォルトメソッドを多用しています。

デフォルトメソッドの使用場面

シンプルそうに見えるデフォルトメソッドですが、実は地味にいろんな場面で役に立っています。

API拡張時の互換性維持

Java 7 以前、java.util.Iterator の定義は以下のようになっています。

// Java 7
public interface Iterator<E>() {
    boolean hasNext();
    E next();
    void remove();
}

Java 8 になって、イテレーションの残り要素に順次処理を適用するメソッド forEachRemaining(Consumer<? super E>) が追加されました。 この処理は他のメソッドの組み合わせで実現可能なので、デフォルトメソッドをインタフェースに追加することで、すべての実装クラスの互換性が維持されます。

// Java 8
public interface Iterator<E>() {
    boolean hasNext();
    E next();
    default void remove() {...}
    default void forEachRemaining(Consumer<? super E> c) {...}
}

実装が必須でないメソッド

前節の java.util.Iterator をもう一回見ましょう。

Java 7 以前、remove() メソッドを提供しない実装クラスでも、例外を投げるメソッドを用意する必要があります。 プログラマにとって、このようなメソッドは関心の対象ではないので、ある意味ただのノイズになります。

// Java 7
class NonRemovableIterator implements Iterator<Object> {
    public boolean hasNext() {...}
    public Object next() {...}
    public void remove() {
        throws new UnsupportedOperationException(”remove”);
    }
}

Java 8 では remove() のデフォルトメソッドが追加されたため、この場合では remove() の実装が不要となりました。 結果として、コードが簡潔になって、関心の対象にも集中しやすくなります。

// Java 8
class NonRemovableIterator implements Iterator<Object> {
    public boolean hasNext() {...}
    public Object next() {...}
}

振る舞いの多重継承と再利用

類似するインタフェースがいくつか存在している場合、デフォルトメソッドを使うとそれらの一般的な振る舞いを再利用*1できます。

少し抽象な例になりますが、生き物とその運動能力の関係を以下のように表して、生き物をクラス、運動能力をインタフェースと見立てます。

f:id:chyiro:20151214170047p:plain

このように、ペンギンが「泳ぐ」と「歩く」、ピューマが「歩く」と「走る」、ペガサスは「飛ぶ」「歩く」「走る」、くも子*2は「泳ぐ」「飛ぶ」「歩く」「走る」ことができます。

運動能力を表すインタフェースにはそれに対応する動きをデフォルトメソッドとして定義すると、生き物たちのクラスは対応するインタフェースを実装するだけで振る舞いを再利用できます。

ただし、このような振る舞いの多重継承は一見便利ですが、前提としてインタフェースのメソッド定義は適切な直交化が必要になります。そうでないと、かえってソースコードの可読性とメンテナンス性を害するオチになります…

メソッド参照の解決ルール

前述したように、デフォルトメソッド導入によって、振る舞いの多重継承が可能になりました。

メソッド実装の参照先が複数存在する状況を解決するため、いくつかの解決ルールが追加されました。自動的にメソッド実装を解決するルールは以下の優先度順になります。

  1. クラスのメソッド実装
  2. 最も特定的なインタフェース*3のデフォルトメソッド

最も特定的なインタフェースが複数存在する場合、上記ルールでは自動的解決できないので、コンパイルエラーになります。この場合は明示的に参照先インタフェースを指定する必要があります。

スーパークラスのメソッド実装が存在する場合、自動解決より明示的に指定した参照先が優先されます。

文章での説明は分かりづらいかもしれないので、簡単な例を見てみましょう。

ルール1: クラスのメソッド実装を使用

クラス C がクラス A を継承し、インタフェース B を実装しています。A と B はそれぞれ hello() の実装とデフォルトメソッドがあります。

class A {
    public void hello() { System.out.println("A"); }
}
interface B {
    default void hello() { System.out.println("B"); }
}
class C extends A implements B {
    // A のデフォルトメソッドを使用
}

この場合、ルール1は適用されて、C の hello() は A の実装を使用することになります。

f:id:chyiro:20151214132905p:plain

ルール2: 最も特定的なインタフェースのデフォルトメソッドを使用

クラス C がインタフェース A とインタフェース B を実装していて、B が A を継承しています。A と B は両方共 hello() のデフォルトメソッドがあります。

interface A {
    default void hello() { System.out.println("A"); }
}
interface B extends A {
    default void hello() { System.out.println("B"); }
}
class C implements A, B {
    // B のデフォルトメソッドを使用
}

この場合、ルール2は適用されて、C の hello() は B の実装を使用することになります。

f:id:chyiro:20151214132912p:plain

番外: 自動解決できない場合

インタフェース A とインタフェース B は両方共 hello() のデフォルトメソッドを持つが、お互い関係ありません。 クラス C が A と B を両方実装していて、インタフェース D が A と B を両方継承しています。

interface A {
    default void hello() { System.out.println("A"); }
}
interface B {
    default void hello() { System.out.println("B"); }
}
class C implements A, B {
    // 実装参照先を自動解決できないので、コンパイルエラーになる
}
interface D extends A, B {
    // 実装参照先を自動解決できないので、コンパイルエラーになる
}

この場合、C と D の hello() の実装参照先を明示的に指定する必要があります。

class C implements A, B {
    public void hello() {
        A.super.hello();  // A のデフォルトメソッドを使用
    }
}
interface D extends A, B {
    public void hello() {
        B.super.hello();  // B のデフォルトメソッドを使用
    }
}

(おまけ)ダイアモンドにまつわる話

多重継承といえば、やはりダイアモンド*4ですね。

Java 7 以前、多重継承できるのは実装を持たないインタフェースだけなので、メソッド実装参照先にあいまい性がありません。したがって、ダイアモンドがあっても、問題は発生しません。

Java 8 にデフォルトメソッドが導入されて、インタフェースにも実装を持つことができるようになりました。ダイアモンド問題は存在しているが、前述した実装参照先の解決ルールを採用することで、解決されました。

f:id:chyiro:20151214142559p:plain

余談ですが、去年(2014)東京での Java Day Tech Night: Ask the Experts では、クラス多重継承またはそれに準じるフィーチャーが追加される可能性があるかという質問が上がったが、エキスパートの方にきっぱりと否定されました。*5

まとめ

デフォルトメソッドは Java 8 のほかの新しいフィーチャーと比べて、あまりファンシーさがなくて影も薄いですが、その存在がほかの新フィーチャーの支えとなっていて、既存コードへのインパクトと導入時コストを抑える役割を見事に果たしています。

今回は主にその性質や使い方に着目して記事にまとめましたが、どのように実現したかは触れていないので、機会があればまたその部分を深く掘り下げたいと思います。

*1:ほかの OOP 言語での mixin の役割と同様です。

*2:雲の上に住んでいるクモの妖精です。

*3:継承関係において最も下位にあるインタフェースを指します。

*4:炭素の結晶体ではなく、かの有名な菱形継承問題のことです。

*5:まあ、Java の設計思想から考えると、その方向はまずありえないでしょう…