2016年10月2日日曜日

[JavaSE8 Goldへの道] その3 インタフェースのデフォルト/staticメソッド


猫と一緒にガジェットライフ♪ムチャです。

JavaSE8 Goldへの道(Upgrade to Java SE 8 Programmer 1Z0-810 試験対策)3回目です。
Java8の追加機能のメインとも言えるStreamAPIに入る前に、インタフェースに追加された「デフォルトメソッド」と「staticメソッド」について解説していきます。

だいぶ長くなってしまったので、Pocketなどご利用下さいませ(;´∀`)



デフォルトメソッド

これまでインタフェースは実装を持つことができませんでした。
インタフェースはあくまで外部に対する窓口(=インタフェース)を定めるものであって、メソッドを定義できるだけで実装はクラスで行う必要がありました。

デフォルトメソッドの書き方

デフォルトメソッドはインタフェースに対し実装を持てるようにしたJava8の新機能です。
使う場合は「default」キーワードをつけます。

public interface DefaultMethod {
  public default void hello(){
    System.out.println("( ´∀`)<ぬるぽ");
  }
}
public class Main implements DefaultMethod {
  public static void main(String[] args) {
    new Main().hello();
    //hello();  ←コンパイルエラー
  }
}
実行結果
( ´∀`)<ぬるぽ

あまり例としては良くないですが・・・。
implementsしたクラスではメソッドの実装を書かなくてもコンパイルは通り、呼び出すことができます。

また、デフォルトメソッドはインスタンスメソッドのような扱いになります。従ってインスタンス経由でないと呼び出すことができません。

「よっしゃこれで既存のクラスにガンガンimplementsして機能拡張したろ!」
というのは終わりの始まりです(;´∀`)
いやできますが・・・。本来Javaのインタフェースとはそういうものではないはず。

デフォルトメソッドが追加された理由

このデフォルトメソッド、そもそもは既存のCollections Framework(ListとかMapとか)に対してラムダ式およびStreamAPI関連の大幅な機能拡張を入れるために追加された仕様のようです。
一番基本となるListやMapなどのインタフェースに新しいメソッドを追加すると、implementsしている全てのクラスで実装を書かないといけなくなりますからね。
参考:

デフォルトメソッドのオーバーライド

デフォルトメソッドはオーバーライドすることもできます。
public class Main implements DefaultMethod {
  @Override
  public void hello() {
    System.out.println("オーバーライドもできる!");
  }
  public static void main(String[] args) {
    new Main().hello();
  }
}
実行結果
オーバーライドもできる!


デフォルトメソッドの使いどころ

追加された理由がAPIの機能拡張のためということから、フレームワークやライブラリの設計者以外はあまり積極的に使うべきではない気がします。

既存の設計を壊さずに機能を追加する、以外に、「匿名クラスで使うんだけど必要なメソッド意外は実装しなくてもOK」という局面(特にリスナー系)で、デフォルトを空実装にしておくとコードがごちゃごちゃにならずに済みそうです。
あるいは実装していなかった場合はUnsupportedOperationExceptionをスローしておく、とかですかね・・・。
public interface SampleDefault {
  public default void empty(){
    //デフォルトで空実装
  }
  public default void required(){
    throw new UnsupportedOperationException("呼んじゃダメ!");
  }
}
参考:

定義できないデフォルトメソッド

ObjectクラスにあるtoString、equals、hashCodeメソッドはデフォルトメソッドとして定義できません。
public interface DefaultMethod {
  //以下は全てコンパイルエラー
  public default String toString(){return null;}
  public default boolean equals(Object o){return false;}
  public default int hashCode(){return 1;}
}
「A default method cannot override a method from java.lang.Object」と出ます。(pleiadesでも訳されていない)
これらのメソッドはそもそもインスタンスの状態を出力したり、状態によって同一性をチェックしたりするものです。インタフェースは状態を持たない(インスタンス変数を定義できない)ので、実装できても意味が無い、という感じのようです。


staticメソッド

デフォルトメソッドに加えて、インタフェースにstaticメソッドも持つことができるようになりました。やはりこれもラムダ式及びStreamAPI追加のために拡張されたようです。

staticメソッドの書き方

public interface StaticMethod {
  public static void hello(){
    System.out.println("staicメソッド!");
  }
}
public class Main implements StaticMethod {
  public static void main(String[] args) {
    StaticMethod.hello();
    //hello(); implementsしていてもメソッド名だけでは呼べない
    //new Main().hello(); //インスタンス経由でも呼べない
  }
}

呼び出す場合はインスタンス名.メソッド名の形で記述します。
実装したクラス内でもメソッド名だけでは呼び出せません。必ずインタフェース名が必要です。
またインタフェースを継承した場合も、サブインタフェース名では呼び出せません。ややこしいですね。
public interface StaticMethod2 extends StaticMethod {}
public class Main {
  public static void main(String[] args) {
    StaticMethod.hello();
    //StaticMethod2.hello(); →コンパイルエラー
   }
}

ちなみに「クラスのstaticメソッド」の場合は実装を持つクラス名でもサブクラス名でも呼び出せます。
コンパイラが以下の警告を出しますが、インスタンス経由でも呼び出せます。
「型 StaticTest からの static メソッド methodA() には static にアクセスする必要があります。」
ただし間違いの元なので、普通はstaticメソッドはクラスやインタフェースの型でアクセスするべきです。

public class StaticTest {
    public static void methodA(){
        System.out.println("クラスのstaticメソッド");
    }
}
public class StaticTestMain extends StaticTest {
  public static void main(String[] args) {
    //スーパークラス名で呼び出し
    StaticTest.methodA();
    //サブクラス名で呼び出し
    StaticTestMain.methodA();
    //メソッド名だけで呼び出し
    methodA();
    //インスタンス経由で呼び出し(警告が出る)
    new StaticTestMain().methodA();
  }
}

通常はeclipseなどのIDEが書いてる側から教えてくれるのであまり気にする必要は無いでしょうが、試験ではこの辺細かく突っ込まれるので覚えておいた方が良いでしょう。
インタフェースのstaticメソッドは型に強く結びつくと考えれば良いかと思います。

staticメソッドは実装が必要です。実装を書いていないとコンパイルエラーとなります。
public interface StaticMethod {
  //コンパイルエラー
  //「このメソッドにはセミコロンの代わりに本文が必要です」
  public static void test();
}


定義できないstaticメソッド

Javaにおいて、staticメソッドをサブクラスで再定義することを、オーバーライドではなく「隠蔽」と呼びます。
そしてstaticメソッドとインスタンスメソッドは相互に隠蔽することができません(コンパイルエラーになります)。

Javaでは全てのクラスはObjectクラスのサブクラスとなっています。インタフェースはクラスではないのでそのスーパークラスは・・・あれ?なんでしょう。

以下のコードはコンパイルできません。
public interface TestInterface {
   //コンパイルエラー
  public static String toString(){return null;}
}
「この staticメソッドは Object からのインスタンス・メソッドを隠蔽できません」というエラーになります。
じゃあということでTestInterface .class.getSuperclass()を呼んでみるとnullが返ってきます。しかしgetSuperclassはインタフェースの場合nullを返す仕様とのことでこれではわかりません。

まあでもなんとなく内部的にはObjectがスーパークラスとして扱われてそうな気がします。eclipseでソース→メソッドのオーバーライド/実装と選択すると、以下のダイアログが出てきます。


ただこれは少しおかしくて、cloneとfinalizeはprotectedなのでインタフェースでは定義できず、@Overrideが付きません。
(インタフェースのメソッドは暗黙に「public abstract」になるため。)

そして、equals,hashCode,toStringは定義だけ(abstract)できますが、staticメソッドにしようとすると上記エラーになるため定義ができません。
100%解決していないような気がしますが、試験を受ける場合は覚えておきましょう。



インタフェースの多重実装

実装の衝突

Javaではクラスを多重継承(extends)することはできませんが、インタフェースは複数実装(implements)することができます。

これまでは特に問題がありませんでした。しかしインタフェースに実装を持つことができるようになったので、以下の場合どうなるでしょうか。
public interface InterfaceA {
  public default void hello(){System.out.println("Aです");}
}
public interface InterfaceB {
  public default void hello(){System.out.println("Bです");}
}
public class MultipleInheritance implements InterfaceA, InterfaceB{
  public static void main(String[] args) {
    new MultipleInheritance().hello(); //どっちが呼ばれる?
  }
}
このコードはコンパイルできません。
「パラメーター () および () を持つ重複した default メソッド hello は型 InterfaceB および InterfaceA から継承されています」というエラーが出ます。
InterfaceAとInterfaceB、どちらのhelloを呼ぶべきか分からないためです。
この場合、implementsするクラスで必ずhelloメソッドをオーバーライドする必要があります。

問題となるのはデフォルトメソッドの場合で、staticメソッドは大丈夫です。前述の通り、staticメソッドはimplementsしたクラスでさえインタフェース名を指定しないと呼び出せないため、衝突しようがないためです。

明示的なデフォルトメソッドの呼び出し

デフォルトメソッドを実装クラスでオーバーライドした場合に、インタフェース側の実装を呼び出すことができます。
public interface InterfaceA {
  public default void hello(){System.out.println("Aです");}
}
public interface InterfaceB {
  public default void hello(){System.out.println("Bです");}
}
public class MultipleInheritance implements InterfaceA, InterfaceB{
  @Override
  public void hello() {
    InterfaceA.super.hello();
    System.out.println("Bさんごめんなさい");
  }
  public static void main(String[] args) {
    new MultipleInheritance().hello();
  }
}
実行結果
Aです
Bさんごめんなさい

インタフェース名.super.メソッド名」で明示的にインタフェースのデフォルトメソッドを呼び出すことができます。
このsuperの前に何かを指定する書き方は今まで無かったと思います。スーパークラスのメソッドを呼び出すときはsuper.メソッド名です。

ちなみにこの形式で呼び出せるのは直接実装したインタフェースのみで、その親のインタフェースのデフォルトメソッドを呼び出すことはできません。
(super.superみたいに指定することはできません。)


クラスとインタフェース間での衝突

インタフェースのデフォルトメソッドとスーパークラスのインスタンスメソッドが衝突した場合、クラスの方が優先されます。
public interface Interface1 {
  public default void test(){
    System.out.println("Interface1");
  }
}
public class SuperCls {
  public void test(){
    System.out.println("super");
  }
}
public class SubCls extends SuperCls implements Interface1{
  public static void main(String[] args) {
    new SubCls().test();
  }
}
実行結果
super

しかし、スーパークラスのメソッドのアクセス修飾子がpublicでない場合はコンパイルエラーとなります。
→「継承メソッド SuperCls.test() は Interface1 内の public 抽象メソッドを隠蔽できません」

インタフェースに定義したメソッドは暗黙に「public abstract」が付きます。
(abstractはデフォルト・staticで実装がある場合を除く)
クラスの場合はサブクラスでメソッドをオーバーライドするときにアクセス修飾子を厳しくすることができません。
インタフェースのデフォルトメソッドとの間も同じ・・・と言いたいところですが、クラス間でのコンパイルエラーは「SuperCls から継承されたメソッドの可視性を下げることはできません」なので、扱いが微妙に異なります(;´∀`)
まあ理由は同じなので、ゆるくするしかできないと覚えておきましょう。



インタフェースの仕様おさらい

暗黙の修飾子

インタフェースで宣言した変数は自動的に「public static final」になります。そのため初期化してないとコンパイルエラーとなります。

メソッドは上に書きましたが「public abstract」になります。ただしdefaultやstaticで実装がある場合はabstractは付きません。

多重継承

クラスは多重継承ができませんが、インタフェースは多重継承ができます。
(知らなかった・・・)
public interface Interface1 {
  public default void test(){}
}
public interface Interface2 {
  public default void test(){};
}
public interface MultipleInterface extends Interface1, Interface2 {
  @Override
  default void test() {
    //衝突した場合は実装が必須
  }
}
デフォルトメソッドが衝突した場合はやはり実装が必須となります。



おしまいのひとこと

曖昧な部分を調べながら書いてたらものすごい長くなってしまいました(;´∀`)
実際には、APIに追加されたインタフェースのデフォルトメソッド・statcメソッドを利用する事はあっても、設計して実装する局面はあまりない気もします。
ちょっとややこしいんだなということだけ覚えておけば、後は調べれば大丈夫でしょう。

一連の記事は「JavaSE8Gold」ラベルを付けていきます。
よろしければおつきあいくださいませ。

それではみなさまよきガジェットライフを(´∀`)ノ


問題集、俗に言う「黒本」が2016年10月14日に出るようです!

▼こちらの記事もどうぞ

▼ブログを気に入っていただけたらRSS登録をお願いします!
▼ブログランキング参加中!応援よろしくお願いします。

スポンサーリンク