ProjectLambda・3部(デフォルト実装)
←2部(ラムダ式(実質的にfinal・スコープ)〜メソッド・コンストラクタ参照)
ここで使用されているサンプルは、lambda-8-b25-05_feb_2012に基づいて作成されています。現在の仕様とは違っている点があるかもしれませんので注意してください。ここまでラムダ式とメソッド参照・コンストラクタ参照について見てきましたが、今回はインターフェースのデフォルト実装についてです。
デフォルト実装(別名:拡張メソッド/デフォルトメソッド)というのはインターフェースのメソッド定義時にデフォルトの実装を定義できるようにし、実装クラスで実装が省略された場合はそのデフォルトの実装が使われるようにするという物です。
デフォルト実装
書き方はメソッド定義のあとにdefault {本体}と書きます。
interface I { void method() default { System.out.println("Default Implementation"); } }
throws節がある場合はthrows節のあとにdefault以降を書きます。
interface I { void method() throws Exception default { throw new Exception(); } }
古い書き方として「default staticMethod」と言ったものがあります。
この書き方に使用されるstaticMethodはstaticなメソッドで、デフォルト実装が使用されるインターフェースを第一引数に受け取り、デフォルト実装をするメソッドの引数を第二引数以降に受け取らなければなりません。
interface I { public static class Defaults { public static void method(I i) { System.out.println("Default Implementation"); } public static void method(I i, int i, double d) { System.out.println(i * d); } } void method() default Defaults.method; void method(int i, double d) throws Exception default Defaults.method; // この書き方でもthrows節のあとにdefault以降を書く }
最新のバイナリでは警告が出ますが利用できます。
このメソッド定義のあとにdefaultと書くデフォルト実装の表記法は、少し気持ち悪いかもしれないですが、アノテーションのデフォルト値の表記法に倣ったものです。
@interface Annotation { String value() default "Default Value"; }
古い書き方とかもろそうですね。
実際に実装してみましょう。
class C1 implements I { // methodを実装せずにデフォルトの実装を使用 } I i = new C1(); i.method(); // Default Implementation class C2 implements I { public void method() // 当然実装してもOK { System.out.println("C2#method"); } } I i = new C2(); i.method(); // C2#method
デフォルト実装とabstract
今までインターフェースに定義されるメソッドはすべてabstract修飾されたものでしたが、デフォルト実装の導入によってこれが変わります。
デフォルト実装されているメソッドは"抽象メソッド"ではなく"デフォルトメソッド"として修飾されます。
そして、"デフォルトメソッド"と"抽象メソッド"は共存できません。
interface I { abstract void method() default {} // Error }
また、関数型インターフェースの定義を思い出すと「SAM-Typeなインターフェース」、つまり「抽象メソッドを一つだけ持っているインターフェース」でした。
デフォルト実装されているメソッドはabstract修飾されていませんのでこの抽象メソッドの頭数としては数えられません。
interface X // 抽象メソッドが一つもないので関数型インターフェースではない { void method() default {} // デフォルトメソッド } interface Y // 抽象メソッドが一つだけ(method1)あるので関数型インターフェースになる { void method1(); // 抽象メソッド void method2() default {} // デフォルトメソッド } X x = () -> {}; // NG Y y = () -> {}; // OK
デフォルト実装とインターフェースの継承
次はIを継承して新しいインターフェースを作ってみます。
interface I2 extends I {}
この場合、I2#methodはI#methodと同じになります。
new I2(){}.method(); // Default Implementation
デフォルト実装を解除したい場合は、abstractメソッドとして再宣言します。
interface I3 extends I { void method(); } interface I4 extends I { abstract void method(); }
これでI3#method、I4#method共にデフォルトメソッドではありません。*1
[2012/03/28:修正:抽象メソッドとしての再宣言の方法について]
デフォルト実装と多重継承
今までJavaはインターフェースには実装を書けないという制約の元、インターフェースを用いた「定義の多重継承」は認めていましたが、「実装の多重継承」は認めていませんでした。
ですが、デフォルト実装によってインターフェースも実装を持てるようになった今、実装の多重継承を認めることになります。
Javaはこの実装の多重継承をどのように処理するのでしょうか。キーワードは「class always wins」―「クラスが常に勝つ」です。
interface I#methodとclass Super#methodがバッティングしているとしましょう。
interface I { void method() default { System.out.println("I"); } } class Super { public void method() { System.out.println("Super"); } }
これらを特にオーバーライドせずに継承・実装するSubというクラスを作ってみましょう。
class Sub extends Super implements I {}
この時Sub#methodはSuper#methodの実装が使われます。
これが「class always wins」―「クラスが常に勝つ」です。
Sub sub = new Sub(); sub.method(); // Super
この「class always wins」のおかげで、Super#methodがfinal修飾されていても問題なくコンパイルできます。
class Super { public final void method() { System.out.println("Super"); } } class Sub extends Super implements I { // OK }
親クラスのメソッドを呼びたい時はsuper.method()と書きましたが、インターフェースの方のデフォルト実装を呼びたい時は
Interface.super.method();
と書きます。
最初のシチュエーションでI#methodの実装を使用してみましょう。
class Sub extends Super implements I { public void method() { I.super.method(); // Iの方の実装を使用する } } new Sub().method(); // I
この様にしてどの実装を使用するかを選択することが出来ます。
最新のコンパイラ(2012/2のバイナリ)だと「Interface.super.method()」はバイトコードレベルでは未実装で、コンパイルできるけど呼び出すとスタックオーバーフローになります。
I#methodのデフォルト実装によって実装の省略が許されていても、Super#methodがabstract修飾されていて抽象メソッドの場合は、SuperとIを継承・実装したクラスでは「class always wins」によってmethodが抽象メソッドとなり実装を省略することはできません。*2
interface I { void method() default {} } abstract class Super { public abstract void method(); } class Sub extends Super implements I { // Sub#methodがabstractになり、Subクラスがabstract修飾されていないのでコンパイルエラー } class Sub2 extends Super implements I { public void method() // Super#methodがabstractなので絶対に実装しなければならない。 { I.super.method(); } }
デフォルト実装とダイアモンド継承
[追記:ここから〜]
複数のインターフェースを継承・実装する場合は少々複雑です。
なぜならば、ダイアモンド継承が起きてしまうためです。
まずはダイアモンド継承ではない例です。
同じシグネチャのメソッドmを持ち、関係性のないAとBを継承するCを作ってみます。(mはAとBの両方でデフォルト実装されている)
interface A { void m() default {} } interface B { void m() default {} } interface C extends A, B {}
この時Cはコンパイルエラーになってしまいます。
なぜなら、A#mとB#mには継承関係が無くメソッドの継承の解決が出来ないためです。
この場合は前途したとおりオーバーライドして呼び出すメソッドを定めます。
interface C extends A, B { void m() default { A.super.m(); } }
次にAとBでmのデフォルト実装をせずに、mがデフォルト実装されたBaseインターフェースを作りAとB両方がmをオーバーライドせずにBaseを継承するとしてみましょう。
interface Base { void m() default {} } interface A extends Base {} interface B extends Base {} interface C extends A, B {}
この場合Cの継承は成功します。
なぜならば、A#mもB#mも共にBase#mなので一意に定まるためです。
次にAだけがmをオーバーライドするとします。
interface A extends Base { void m() default {} } interface C extends A, B {}
この場合も同様でCの継承は成功します。
なぜかというと、インターフェースはダイアモンド継承だけどm自体は単一継承の様になるためです。
まず、Bはmのデフォルト実装をオーバーライドしていないのでBase#mと同様に扱われます。
そしてA#mのメソッドはBase#mを継承したとして扱われるので、C#mは次のようになります。
Base#m <- A#m <- C#m
そしてC#mは最も近いA#mになります。
ただBもオーバーライドする場合はmもダイアモンド継承になるのでCの継承はコンパイルエラーとなってしまいます。
interface A extends Base { void m() default {} } interface B extends Base { void m() default {} } interface C extends A, B {} // エラー
最初の例と同じようにCでmをオーバーライドしなければなりません。
[〜ここまで2012/02/28]
デフォルト実装の使いどころ
1.誰が実装しても同じような実装になるとき
これはもろデフォルト実装が活きるシチュエーションでしょう。
例えば次のようなファイルから読み込みを行うインターフェース、Loaderがあったとしましょう。
public interface Loader<T> { T loadFrom(File file) throws IOException; T loadFrom(String fileName) throws IOException; // loadFrom(File)へのゲートウェイメソッド }
これを実装するクラスの多くはloadFrom(String)について次のような実装をするでしょう。
public class StudentLoader implements Loader<Student> { public Student loadFrom(File file) throws IOException { try (FileInputStream = ...) { ゴニョゴニョ } } public Student loadFrom(String fileName) throws IOException { return this.loadFrom(new File(fileName)); } }
こういった物はデフォルト実装で実装してしまいましょう。
public interface Loader<T> { T loadFrom(File file) throws IOException; T loadFrom(String fileName) throws IOException default { return this.loadFrom(new File(fileName)); } }
こうしておけば実装クラスではloadFrom(File)だけを実装すればよいことになります。
あるいは、テンプレートメソッドパターンなどにも使用できるかも。(まぁ、普通は抽象クラスを使うんだろうけど・・・)
public interface Template { void open(); void print(); void close(); void displayTimes(int time) default { this.open(); for (int i = 0; i < time; i++) { this.print(); } this.close(); } }
2.任意のオペレーションのメソッドを定義する時
今まで任意のオペレーションを定義するときはJavadocに「任意のオペレーションです」とか「@throws UnsupportedOperationException このオペレーションをサポートしない時。」などと記す以外に伝える手段がありませんでした。
そしてそのオペレーションをサポートしない実装クラスではわざわざオーバーライドしてUnsupportedOperationExceptionなどを投げる処理を書かなければなりませんでした。
こういったものはデフォルト実装を用いてデフォルトだとUnsupportedOperationExceptionなどのサポートしない時の処理を書いておきましょう。
public interface Iterator<E> { boolean hasNext(); E next(); void remove() default { throw new UnsupportedOperationException("This operation is unsupported"); } }
こういった感じの実装が実際にCoreAPIのIteratorで行われています。
3.既存のインターフェースに新しいメソッドを追加する時
OCPに基づいて拡張はサブインターフェースで行うべきですが、どうしてもそのインターフェースで行わなければならないときがあります。
現実的に、ProjectLambdaによってIterableには多くのメソッドが追加されます。
もしそういったときに普通に定義した場合、大量の既存のクラスで大量のコンパイルエラーが出て、にっちもさっちも行かなくなるでしょう。
こういう時こそ、デフォルト実装です。
// 拡張前 public interface Loader<T> { T loadFrom(File file); } //拡張後 public interface Loader<T> { T loadFrom(File file) throws IOException; T loadFrom(String fileName) throws IOException default // 1の方針でデフォルト実装を使用する { return this.loadFrom(new File(fileName)); } /** * 任意のオペレーション * @throws UnsupportedOperationException この操作がサポートされていないとき * @throws IOException */ T loadFromNet(URL url) throws IOException default // 2の方針でデフォルト実装を使用する { throw new UnsupportedOperationException(); } }
この様にしておけば既存の実装クラス群は修正しなくても済みます。
拡張メソッドと呼ばれる所以はこのインターフェースを拡張するという所にあります。
デフォルト実装を導入するのは3番の問題を解決するためです。
さて、次回は例外ジェネリクスです。例外ジェネリクスでProjectLambdaにおける言語仕様の変更はラストになります。
このデフォルト実装でProject Lambdaにおける大きな言語仕様の変更はラストとなります。
2012/02/28:追記:インターフェース間の継承に言及
2012/03/28:修正:抽象メソッドとしての再宣言の方法について(default noneについては削除)
2012/04/11:修正:例外ジェネリクスが無くなったのに伴い本文を修正