広告

Java : インタフェースの default メソッドとは

Java 8 から、インタフェースに default メソッドが追加されました。
これにより、限定的ですがインタフェースに実装を含めることができます。

しかし、どういった場面で使えばよいのでしょうか?

本記事では

  • default メソッドは、なぜ Java 言語仕様に追加されたのか?
  • 抽象クラスとの違いは?

などを紹介しつつ、使うべきケースについて解説します。


概要

インタフェース (英: interface) は、JavaやC#などのオブジェクト指向プログラミング言語においてサポートされる、実装を持たない抽象型のことである。これらの言語において、クラスは実装の多重継承をサポートしない代わりに、任意の数のインタフェースを実装 (implement) することができ、これにより型の多重継承をサポートする。

インタフェースは、通常、実装を持ちません。
実際、Java 7 まではそうでした。

しかし、Java 8 で default メソッドが言語仕様に追加されました。
これにより、限定的ではありますがインタフェースにも実装が持てるようになりました。

具体的な例で見てみましょう。

次のような A というインタフェースがあります。

interface A {
    void func();
}

インタフェース A は func というメソッドを持ちますが、実装は持ちません。
よって、A を継承(implements) するクラス X は、必ず func メソッドを実装する必要があります。

class X implements A {
    @Override
    public void func() {
        System.out.println("Class X : OK!");
    }
}
final A a = new X();
a.func();

// 結果
// ↓
//Class X : OK!

もし func メソッドを実装しないとコンパイルエラーとなります。

interface A {
    void func();
}

// コンパイルエラー
class X implements A {
}

次に default メソッドの例を見てみましょう。
default メソッドを使うには、default キーワードを使います。

interface B {
    default void func() {
        System.out.println("Interface B : OK!");
    }
}

インタフェース B を継承(implements) するクラスは、func メソッドの実装を省略することができます。

class Y implements B {
}
final B b = new Y();
b.func();

// 結果
// ↓
//Interface B : OK!

クラス Y は func メソッドの実装を省略しています。
この場合、インタフェース B の default メソッドによる処理が実行されます。

もちろん、今までどおり func メソッドをオーバーライドすることもできます。

interface B {
    default void func() {
        System.out.println("Interface B : OK!");
    }
}

class Z implements B {
    @Override
    public void func() {
        System.out.println("Class Z : OK!");
    }
}
final B b = new Z();
b.func();

// 結果
// ↓
//Class Z : OK!

以上が、ざっくりとした default メソッドの紹介になります。
なんとなくイメージはできましたでしょうか…?

ノート

  • Java 9 では、実装を持つ private メソッドがインタフェースに定義できるようになりました。
    private メソッドについては「補足」にて軽く解説しています。

なぜ必要なのか?

進化するJavaインタフェース

上記の資料は、オラクルのサイトで公開されているドキュメントです。

とても丁寧に、

  • なぜ default メソッドが必要になったのか?

が解説されています。
日本語に翻訳されているので、ぜひ一読することをおすすめします。

簡単にまとめると、default メソッドが必要になった理由は、

  • 下位互換性を保ったままインタフェースに新規メソッドを追加する

という目的のためです。

具体的な例で考えてみましょう。

A さんは、Java のアプリを開発しています。
その自作アプリでは、Java の標準ライブラリである List インタフェースを独自に実装した MyList クラスを作って利用していました。

このとき、標準ライブラリは Java 7 を使いました。
Java 7 のコンパイラを使って、ビルドも問題なし。アプリも無事に動きました。

MyList1

// 標準ライブラリ

public interface List<E> extends Collection<E> {
    ... 省略 ...
}
// 自作アプリ

public class MyList implements List<String> {
    ... 省略 ...
}

さて、しばらくして Java 8 がリリースされました。
仮に、Java 8 では List インタフェースに sort メソッドが新規に追加されたとします。

Java は基本的に下位互換性があります。
よって、特に自作アプリに修正を入れなくても、Java 8 で問題なくコンパイルはとおるはずです。

しかし、default メソッドなしで List に sort メソッドが追加されたとすると、自作アプリのビルドは失敗します。
なぜなら MyList クラスに sort メソッドの実装が必要になるからです。

MyList2

// 標準ライブラリ

public interface List<E> extends Collection<E> {
    ... 省略 ...

    // 新規に追加
    void sort(Comparator<? super E> c);
}
// 自作アプリ

public class MyList implements List<String> {
    ... 省略 ...

    // ↓ 新たに実装が必要
    @Override
    public void sort(Comparator<? super String> c) {
        ... 省略 ...
    }
}

これは、Java の標準ライブラリをリリースする側からすると困りますよね。
なぜなら、下位互換性を保つためには、1回リリースしたインタフェースを永遠に変更することができないのですから…

この問題に対応するために、default メソッドは追加されました。

先ほどの List の sort メソッドも、default メソッドとして追加されていれば、MyList クラスを特に変更しなくてもビルドは問題なくとおります。

MyList3

// 標準ライブラリ

public interface List<E> extends Collection<E> {
    ... 省略 ...

    // ↓新規に追加
    default void sort(Comparator<? super E> c) {
        ... 省略 ...
    }
}

抽象クラスとの違い

抽象型(ちゅうしょうがた、英: abstract type)とは、コンピュータプログラミングの型システムのうち、名前的型システム (nominal / nominative type system) における型の一種であり、直接インスタンス化することができないという特徴を持つ。

default メソッドが追加されたため、インタフェース(interface) と 抽象クラス(abstract class) との違いが少なくなりました。
どちらも抽象型といえますしね。

とはいえ、明確な違いもあります。

主な違いは…

  • インタフェース
    • 多重継承(implements) ができる
    • フィールド変数が持てない
    • フィールドのアクセス修飾子は public のみ (通常省略して表記する)
    • メソッドのアクセス修飾子は public (通常省略して表記する) と private が使える

  • 抽象クラス
    • 単一継承(extends) のみ
    • フィールド変数を持てる
    • フィールドとメソッドのアクセス修飾子はすべて使える

です。

コード例でも見てみましょう。

インタフェース 抽象クラス
継承
interface A {
}

interface B {
}

// 多重継承(implements) OK
class X implements A, B {
}
abstract class A {
}

// 単一継承(extends) のみ OK
class X extends A {
}
フィールド
interface A {
    // 定数(public static final) は OK
    public static final String NAME_1 = "abc";

    // 通常、修飾子は省略します。
    // 省略した場合は public static final となります。
    String NAME_2 = "XYZ";
}
abstract class A {
    // 変数 OK
    private String name = "abc";

    void func() {
        // 変数なので変更も可能
        name = "XYZ";
    }
}
アクセス修飾子
public interface A {
    // public OK
    public default void func1() {
    }

    // 通常、アクセス修飾子は省略します。
    // 省略した場合は public となります。
    default void func2() {
    }

    // private OK
    private void func3() {
    }

    // protected は NG (コンパイルエラー)
    protected default void func4() {
    }
}
abstract class A {
    public abstract void func1();

    // public 以外も OK
    protected abstract void func2();
}

これらの違いを意識して、使い分けていきたいですね。

使うべきケースは?

default メソッドの使うべきケースを考えてみましょう。
個人的には、そこまで積極的に使わなくてもよいのかな…と考えています。

気を付ける点は、default メソッドは必ず public になるということです。
つまり全て公開されます。

もし public にしたくないメソッドであれば、それは default メソッドにするべきではありません。
代わりに、抽象クラス を使うことを検討してみましょう。


デフォルトの処理

デフォルト・メソッドという名前のとおり、インタフェースのデフォルトとしての処理を書きましょう。

例えば、次のような Client と Callback インタフェースがあるとします。

interface Client {
    void request(Callback callback);
}
interface Callback {

    void onSuccess();

    void onFileError();

    void onNetworkError();

    void onDateError();
}

Client インタフェースの request メソッドには、処理の結果を受け取る Callback インタフェースを指定します。

処理が成功したときには onSuccess が呼び出されて、処理に失敗したときはエラーの種別に応じた on~Error が呼び出されるイメージです。
そして、on~Error のメソッドがたくさんあったとしましょう。(例では on~Error は3つです)

それでは、Client の request メソッドを呼び出してみましょう。
Callback は、すべてのメソッドを @Override して実装する必要があります。

final var client = ...;

// 処理をリクエスト
client.request(new Callback() {
    @Override
    public void onSuccess() {
        ... 実装 ...
    }

    @Override
    public void onFileError() {
        ... 実装 ...
    }

    @Override
    public void onNetworkError() {
        ... 実装 ...
    }

    @Override
    public void onDateError() {
        ... 実装 ...
    }
});

すべてのエラー処理が必要なところはこれでよいでしょう。

しかし、場合によってはエラー処理が不要なリクエストもあるかもしれません。
その場合でも毎回 on~Error をすべて実装するのは面倒です。

そんなときは default メソッドが使えます。

interface Callback {

    void onSuccess();

    default void onFileError() {
    }

    default void onNetworkError() {
    }

    default void onDateError() {
    }
}

エラー処理用のメソッドはすべて default にしました。
今回は default の中身は空です。エラーでもログだけは出力させたい、という場合はその処理をいれてもよいかもしれませんね。

final var client = ...;

// 処理をリクエスト
client.request(new Callback() {
    @Override
    public void onSuccess() {
        ... 実装 ...
    }
});

今回のリクエストではエラー処理は不要ということで、onSuccess だけを @Override します。
だいぶすっきりしました。

もし on~Error が呼び出されたら、default メソッド(今回は空の処理) が実行されます。


他のメソッドに依存した処理

具体的な例で見てみましょう。

interface Book {

    String getTitle();

    // タイトルを大文として取得します。
    default String getTitleUpperCase() {
        return getTitle().toUpperCase();
    }
}

Book インタフェースは、本のタイトルを取得する getTitle メソッドを持ちます。
そして、getTitleUpperCase は、タイトルを大文字として取得します。

注目する点としては、getTitleUpperCase メソッドの中で getTitle メソッドを使っていることです。
つまり、getTitleUpperCase メソッドは Book インタフェースのメソッドに依存しているというわけですね。

class BookImpl implements Book {
    @Override
    public String getTitle() {
        return "Hello world!";
    }
}

Book インタフェースを継承(implements) した BookImpl クラスでは、getTitle メソッドだけ実装します。

final Book book = new BookImpl();

System.out.println(book.getTitle());
System.out.println(book.getTitleUpperCase());

// 結果
// ↓
//Hello world!
//HELLO WORLD!

無事に、getTitleUpperCase では大文字に変換されたタイトルが取得できました。
この使い方の default メソッドは、基本的には @Override されることはないでしょう。

もしくは、default メソッドの代わりに、別途ヘルパークラスやユーティリティクラスを作ってしまってもよいかもしれません。
あまりインタフェースにメソッドが多くなるのもあれですし…

ヘルパークラスの例:

interface Book {
    String getTitle();
}

class BookHelper {
    private BookHelper() {
    }

    static String getTitleUpperCase(Book book) {
        Objects.requireNonNull(book);
        return book.getTitle().toUpperCase();
    }
}

下位互換性を保つ

なぜ必要なのか?」のところでも述べましたが、

  • 下位互換性を保ったままインタフェースに新規メソッドを追加する

というのは、default メソッドが追加された主な理由ですね。

とはいえ、このケースで使われることはあまり多くはないでしょう。
このケースが必要になるのは、Java 標準ライブラリのように、不特定多数に向けてライブラリを公開しているかたになります。

もし、インタフェースに新規メソッドを追加して、継承(implements) 先のクラスもそのまま対応できるのであれば、default メソッドを使わずに対応してしまったほうがよいでしょう。

U1

必要のない実装をインタフェースに持たせるのは、あまりおすすめはしません。
インタフェースは、できるだけシンプルな状態を保ったほうがよさそうです。


補足

private メソッド

An interface method lacking a private, default, or static modifier is implicitly abstract. Its body is represented by a semicolon, not a block. It is permitted, but discouraged as a matter of style, to redundantly specify the abstract modifier for such a method declaration.

Java 9 から、インタフェースに private メソッドが導入されました。

private メソッドは、必ず実装を含めなければなりません。
つまり abstract にはできません。このあたりは default メソッドと同じですね。

ただし、private メソッドに default キーワードは不要です。

interface A {
    default void func1() {
        System.out.println("default : OK!");
        func2();
    }

    private void func2() {
        System.out.println("private : OK!");
    }
}

class X implements A {
}
A a = new X();
a.func1();

// 結果
// ↓
//default : OK!
//private : OK!

また、private なので継承(implements) 先のクラスで @Override できません。

class X implements A {
    // default メソッドはオーバーライドOK
    @Override
    public void func1() {
    }

    // private メソッドはオーバーライドできない (コンパイルエラー)
    @Override
    public void func2() {
    }
}

多重継承による問題

クラスに複数のスーパークラスを持たせることを多重継承という。単一継承と異なり、多重継承では、スーパークラス上のメンバサーチが複数方向に分かれるので、どのメンバが参照されるのかの把握が困難になるという欠点がある。

複数のクラスを継承することを多重継承といいます。

クラスの多重継承は便利なこともありますが、注意して使わないと問題も発生します。
多重継承による具体的な問題については、ここでは割愛します。

興味のあるかたは、上記の Wikipedia や 菱型継承問題 などを調べてみるのもよいかもしれません。

Java では、多重継承の問題を回避するために、次のような方針をとりました。

  • クラス(実装を持つ) の多重継承(extends) は許可しない
  • インタフェース(実装を持たない) の多重継承(implements) は許可する

ここまでは default メソッドが追加される前の話です。

さて、default メソッドが追加されたことにより、実装を持つ インタフェースによる多重継承が可能となりました。
コード例で見てみましょう。

interface A {
    default void func() {
        System.out.println("A : OK!");
    }
}

interface B {
    default void func() {
        System.out.println("B : OK!");
    }
}

インタフェースの A と B は、まったく同じ型を持つ default の func メソッドが定義されています。
この A と B を多重継承(implements) してみましょう。

// コンパイルエラー
class X implements A, B {
}
final var x = new X();
x.func();

結果は、コンパイルエラーとなります。

X の func メソッドを呼び出しても、それは A の func を呼び出すべきか、B の func を呼び出すべきか判断できないためです。
よって、コンパイルエラーを解決するには、クラス X で func メソッドを @Override する必要があります。

class X implements A, B {
    @Override
    public void func() {
        // B の func を呼び出します。
        B.super.func();
    }
}
final var x = new X();
x.func();

// 結果
// ↓
//B : OK!

このように、多重継承によるメソッドの呼び出しが解決できないときは、Java ではコンパイルエラーとなります。

ノート

  • プログラミング言語によっては、エラーとならずにいい感じのメソッドを自動的に決定するものもあります。(Pythonなど)
  • 個人的には、Java のようにエラーとして扱ってくれたほうが明確になって好きですね。

公式ドキュメント

Default methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older versions of those interfaces.

英語になりますが、公式ドキュメントによるデフォルト・メソッドのチュートリアルです。
(残念ながら公式の日本語訳はなさそう…)

余裕があれば、こちらも確認するのがよさそうです。

まとめ

本記事では、インタフェースの default メソッドについて解説しました。
個人的には、そこまで積極的に使わなくてもよいのかな、と思っています。

とはいえ、便利なこともあるので、必要になったときは有効に活用していきたいですね。


関連記事

ページの先頭へ