プログラミング応用b 第04回 『インタフェイス(interface )と実装2』 -00~interfaceの新機能

●今回は, interface に追加された機能
   ・interfaceでstaticメソッドを定義できる
   ・実装を持つdefaultメソッド
 について説明していく。


 前回学習したように,本来,interface内には,
  ・publicな抽象メソッド
  ・publicかつstatic finalな定数
しか定義できない。

しかし,Java 8 (2014年)のバージョンから,interface内に
  ・staticメソッド
  ・実装を持ったpublicな非staticメソッド
を定義できるようになった。

まず,interface内で定義できるようになったstaticメソッドから紹介する。



【interface内で定義するstaticメソッド】
 ・Java 8 からstaticメソッドをinterface内で定義できるようになった。
 ・アクセス修飾子として,publicかprivateが選べる。何も付けなければ自動的にpublicと見なされる。
  なお, privateが指定できるようになったのは Java 9 (2017年) から。
 ・interface内で定義されたstaticメソッドは,実装するクラスに継承されない。
  そのため,実装クラス側で使用したいときは,
    interface名.staticメソッド名( 実引数の並び )
  として呼び出さなくてはならない。

 ・interfaceには基本的に実装部分がないので,他のstaticメンバを利用するか,何かしらのオブジェクト
  を実引数として受け取って利用するなどして処理を行うことになる。

●interfaceにおけるstaticメソッドの使用例

 文字列で表された数値を計算する計算機ソフトを作成することを考えよう。

  (1)パッケージa
   ・ 計算機の機能を抽象メソッドとして揃えた interface として Calculator を定義する。
   ・ Calculatorの実装クラスとして,全角数字文字からなる数値も計算できる JapaneseCalculator クラスを定義する。
    しかし,この実装クラス JapaneseCalculator の詳細は,その名前も含めて他のパッケージには公開しない。

  (2)パッケージb
   ・Calculator型オブジェクトを使って計算を行う。

以下がそのプログラム例である。

パッケージa の Calculator.java (staticメソッド getInstance は,Calculatorの実装クラスであるJapaneseCalculator型のオブジェクトを返す。)
package a; // パッケージa

public interface Calculator {
    double add( ); // 2つの数値の足し算をして結果を返す抽象メソッド
    double sub( ); // 2つの数値の引き算をして結果を返す抽象メソッド
    double mul( ); // 2つの数値の掛け算をして結果を返す抽象メソッド
    double div( ); // 2つの数値の割り算をして結果を返す抽象メソッド
    static Calculator getInstance( String a, String b) {
        return new JapaneseCalculator( a, b ); // JapaneseCalculator型オブジェクトを返す。
    }
}


パッケージa の JapaneseCalculator.java
package a;

//このクラスは publicクラスでは無いので,他のパッケージから見えない(アクセス出来ない)。
class JapaneseCalculator implements Calculator { // 全角文字で計算可能な JapaneseCalculator クラス。
    private String a, b; // 2つの数値を表す数字の並んだ文字列
    void setA( String a ){ this.a = a; }
    void setB( String b ){ this.b = b; }
    JapaneseCalculator( String a, String b ) { setA(a); setB(b); }
    
    // 全角文字数字を半角文字数字へ置換し,double型数値に変換して返す。
    private static double convert( String s ) {
        String r;
        r = s.replace("0", "0");
        r = r.replace("1", "1");
        r = r.replace("2", "2");
        r = r.replace("3", "3");
        r = r.replace("4", "4");
        r = r.replace("5", "5");
        r = r.replace("6", "6");
        r = r.replace("7", "7");
        r = r.replace("8", "8");
        r = r.replace("9", "9");
        r = r.replace(".", "."); // 小数点
        
        return Double.valueOf(r); // Double型のvalueOfメソッドを使って,文字列表現をdouble型数値に変換して返す。
    }
    public double add( ) {
        return convert(a) + convert(b);
    }
    public double sub( ) {
        return convert(a) - convert(b);
    }
    public double mul( ) {
        return convert(a) * convert(b);
    }
    public double div( ) {
        return convert(a) / convert(b);
    }
}


パッケージbのテスト用Test.java
package b; // パッケージB
import a.*;

class Test {
    public static void main ( String [ ] args ) {
        // 以下はエラーになる。これは,パッケージaのJapaneseCalculatorはパッケージbからは見えない(アクセス出来ない)ため。
        // Calculator dentaku = new JapaneseCalculator( "10", "20" );

        // かわりに,オブジェクトを生成・初期化するstaticメソッド getInstance を呼び出して JapaneseCalculator型オブジェクトを得る。
        // これによって,JapaneseCalculatorの詳細を他パッケージから隠しながら,JapaneseCalculator型オブジェクトを提供できる。
        Calculator dentaku = Calculator.getInstance( "10", "20" );
        System.out.println(dentaku.add()); // 足し算結果を表示。
        System.out.println(dentaku.sub()); // 引き算結果を表示。
        System.out.println(dentaku.mul()); // 掛け算結果を表示。
        System.out.println(dentaku.div()); // 割り算結果を表示。
    }    
}


このように,オブジェクトを生成するstaticメソッドを持つinterface は,Javaのライブラリにも散見される。

interface内staticメソッドの他の使用例は,次に説明する defaultメソッドの使用例で示す。




【interface内で定義するdefaultメソッド】
 ・interface内で,頭に default というキーワードをつけることによって,実装を持つ非staticメソッドを定義できる。
  このメソッドを defaultメソッド と呼ぶ。
  ※publicというキーワードをつけなくても,自動的にpublicメソッドとして扱われる。
 ・defaultメソッドは実装を持っている(=抽象メソッドではない)ので,実装クラス側でオーバライドしなくても良い。
 ・interfaceには基本的に実装部分がないので,処理の中で非staticフィールドなどは利用できず,主にstaticメソッド,
  抽象メソッドを利用することになる。

定義例:DefaultMethodTest.java

interface Drawable {
    default void draw() { System.out.println( "(^o^)" ); }
}

// 以下のPersonクラスは,drawメソッドをオーバーライドしないが,抽象クラスとして定義しなくても良い。
class Person implements Drawable {
}

public class DefaultMethodTest {
    public static void main ( String [ ] args ) {
        Person p = new Person( );
        p.draw( ); // "(^o^)"と表示される。
    }
}


次に,より実践的な例を示す。

●interfaceにおけるdefaultメソッドの使用例

次の様な構成を考えてみよう。
 ・挨拶が出来る事を表すinterface Greetable。挨拶を行うメソッド greet を defaultメソッドとして定義する。
 ・Greetableを実装する Personクラス。
 ・動作テスト用のmainメソッドを持つTestクラス。

挨拶が出来る事を表す interface Greetable (Greetable.java)
import java.time.LocalTime;

public interface Greetable { // 挨拶ができることを表すインタフェイスGreetable
    // 時刻によって違う挨拶メッセージを返す下請けstaticメソッド。
    static String getGreetingMessage( ) {
        LocalTime time = LocalTime.now(); // 現在時刻を表すLocalTime型オブジェクトを取得。
        int hour = time.getHour(); // 何時(0~23)か取得する。
        String str;
        if( hour >= 4 && hour < 10 ) { // 朝4時以降,10時前まで
            str = "おはようございます。";
        }
        else if( hour >= 10 && hour < 18 ) { // 朝4時以降,夕方18時前まで
            str = "こんにちは。";
        }
        else { // それ以外 (夕方18時以降,朝4時前まで)
            str = "こんばんは。";
        }
        return str;
    }
    
    default void greet( ) { System.out.println( getGreetingMessage() ); }
}

※LocalTimeはその地域の時間帯の時刻を表すクラス。デフォルトコンストラクタで,生成時の時刻に初期化される。
※staticメソッド getGreetingMessage がdefaultメソッド greet の下請けメソッドになっている。
※defaultメソッドgreetは,時刻によって異なる挨拶をするようにデフォルトの実装を与えられている。

人を表すPersonクラス (Person.java)
class Person implements Greetable {
    int age;
    int gender; // 0は男性,1は女性
    public Person ( int age, int gender ) {
        this.age = age; this.gender = gender;
    }
}

※ Personクラスは Greetableを実装しており,greetメソッドはオーバライドせずにデフォルトの実装をそのまま継承している。

動作テスト用のmainメソッドを持つクラスTest (Test.java)
class Test {
    public static void main ( String [ ] args ) {
        Person p1 = new Person( 20, 0 ); // 20歳の男性
        Person p2 = new Person( 19, 1 ); // 19歳の女性
        p1.greet();
        p2.greet();
    }
}


実行結果は以下の様になる(朝の時間帯の場合。男性も女性も同じ挨拶をする)。
おはようございます。
おはようございます。

次に,Personクラスで greet メソッドをオーバライドして,男女で挨拶を変えてみよう。

改造版のPersonクラス (Person.java)
class Person implements Greetable {
    int age;
    int gender; // 0は男性,1は女性
    public Person ( int age, int gender ) {
        this.age = age; this.gender = gender;
    }
    
    // インタフェイス Greetable から継承した greet メソッドをオーバライド。
    // 性別によって異なる挨拶をするようにする。
    public void greet( ) {
        String greeting = Greetable.getGreetingMessage( ); // interfaceのstaticメソッドは継承されないので クラス名. をつけて呼び出す。
        String greeting2 = "";
        
        if( gender == 0 ) { // 男性の場合
            greeting2 = "調子はどうッスか?";
        }
        else if( gender == 1 ) { // 女性の場合
            greeting2 = "ごきげんいかが?";            
        }
        
        System.out.println( greeting );
        System.out.println( greeting2 );
    }
}
※Greetableのstaticメソッドを呼び出している部分が, Greetable.getGreetingMessage( ) になっているところに注意。
 前述したように,interfaceのstaticメソッドは実装クラスに継承されないので, getGreetingMessage( )だけでは呼び出せず,
 前に interface名を指定して呼び出さなければならない。

実行結果は,次の様になる(朝の時間帯の場合。男性か女性で異なる挨拶をする)。
おはようございます。
調子はどうッスか?
おはようございます。
ごきげんいかが?


【実装の衝突が起こった場合の解決策】
 defaultメソッドは実装を持っているので,以下の様に実装の衝突が起こる場合がある。

実装の衝突が起こる例:ImplementationConflict.java
interface A {
    default void f( ) {
        System.out.println( "A" );
    }
}

interface B {
    default void f( ) {
        System.out.println( "B" );
    }
}

class C implements A, B { // Aのf( ) と BのF( ) の実装が衝突するのでエラーになる。

}

public class ImplementationConflict {
    public static void main ( String [ ] args ) {
        C c = new C( );
        c.f( );
    }
}


このように実装の小津が起こった場合,以下の様に実装クラスでメソッドをオーバライドしてやれば実装の衝突は回避され,エラーは出なくなる。

実装の衝突を回避するように改善した例:ImplementationConflict.java
interface A {
    default void f( ) {
        System.out.println( "A" );
    }
}

interface B {
    default void f( ) {
        System.out.println( "B" );
    }
}

class C implements A, B {
    public void f( ) { // f( )をオーバライドして実装の衝突を回避した。
        System.out.println( "C" );
    }
}

public class ImplementationConflict {
    public static void main ( String [ ] args ) {
        C c = new C( );
        c.f( );
    }
}

実行結果:
C