プログラミング応用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
   ・パッケージaを利用する側。Calculator型オブジェクトを使って計算を行う。

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

■interface の staticメソッド例1

 Java の java.lang.Math クラスには,数学関連の定数や数学関数などの計算を行うのに便利なメソッドが(publicな) static メンバとして用意
されている。Mathクラスは,以下の様に定義されている。 (classキーワードの前の finalキーワード がついているが,このように finalキーワード
つきで定義されたクラスはスーパークラスに指定できない。)
package java.lang;
public final class Math {
    static double cos( double a ) { /* 略 */ } // コサインの計算。
    static double sin( double a ) { /* 略 */ } // サインの計算。
    static double max( double a, double b ) { /* 略 */ } // 大きい方の値を返す。
    static double min( double a, double b ) { /* 略 */ } // 小さい方の値を返す。
    static double random( ) { /* 略 */ } // 0.0以上,1.0未満の乱数を返す。
    static double sqrt( double a ) { /* 略 */ } // 正の平方根を返す。
    // 以下略
}

java.lang.Math クラスは,Math型のオブジェクトを生成することを意図しているのではなく,このように便利な道具としてのstaticメソッドを
まとめて提供する目的で用意されている。

interface にも static メソッドが定義可能になったため,便利な道具としての static メソッドを interfade に用意できるようになった。
以下にその例を示す。

この例は,描写可能であることを表すインタフェイス Drawable に,描写の処理を手助けする下請けの static メソッドを用意している。
具体的には,Drawableインタフェイスを実装するクラスで drawメソッドをオーバライドするわけだが,その処理の中でこういった
static メソッドを利用できるだろう。

interface Drawable { // 描写可能であることを表すインタフェイス。
    void draw( ); // 描写処理を実行するメソッド。
    // 以下は,描写に使える下請けメソッド。
    static void drawLine( double x1, double y1, double x2, double y2 ) { /* 略 */ } // 点(x1, y1)から点(x2, y2)まで直線を描写する。
    static void drawRect( double x1, double y1, double x2, double y2 ) { /* 略 */ } // 点(x1, y1)を左上の頂点,点(x2, y2)を右下の頂点とする長方形を描写する。
    static void drawCircle( double x, double y, double r ) { /* 略 */ } // 点(x, y)を中心とした半径rの円を描写する。
    // 以下略
}



■interface の staticメソッド例2

少し高度な使い方だが,これはまず以下のコード例を見て欲しい。

パッケージ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);
    }
}


こうして定義された Calculator インタフェイス型のオブジェクト(実際は,Calculator インタフェイスを実装する具体クラスのオブジェクト)は,
パッケージaの中で様々な処理に利用されるとする。

一方,提供されたパッケージaを利用する側のパッケージbでは,以下のように Calculator インタフェイスを実装するクラスを自分でいちいち定義
しなくても,Calculator インタフェイスを実装した「典型的なオブジェクト(インスタンス)」を,static メソッド Calculator.getInstance
を呼び出すだけで,手軽に利用できる。

パッケージ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()); // 割り算結果を表示。
    }    
}

こうすることで,パッケージaの提供者(開発者)は,パッケージaの利用者に対し,

 (i) 「文字列表現の数値を計算する」計算機の機能をまとめた Calculator インタフェイスを提供できる。
   パッケージaの利用者は,Calculator インタフェイスを実装する具体クラスを独自に定義することで,
   ・ 特定の数字の文字列表現に対応した計算を行える具体クラスを定義でき,
   ・ そのオブジェクトは Calculator 型オブジェクトとして,パッケージaの処理に渡せる。

 (ii) Calculator インタフェイスの「典型的な実装クラスのオブジェクト」を利用者に提供できる。
   例:上記のコード例だと,パッケージaの利用者は,Calculator.getInstance メソッドを呼び出すだけで全角数字文字を使って計算できる
     Calculator型オブジェクト(実際にはJapaneseCalculator型オブジェクト )を利用できる。パッケージaの利用者の目的がこれで済むなら,
     パッケージaの利用者は,Calculator インタフェイスを実装するクラスをいちいち自分で定義する必要はない。

 (iii) パッケージaの利用者に影響が出ない形で,「典型的な実装クラスのオブジェクト」を改良できる。
   例:パッケージaの提供者(開発者)は,JapaneseCalculator クラスを,全角数字だけでなく漢字の数字を使った数値表現(例:"千二百")も
     受け取って計算できる KanjiCalculator クラスにバージョンアップして,Calculator.getInstance メソッドが KanjiCalculator 型
     オブジェクトを返すように変更しても,パッケージaの利用者側(上記の例ではパッケージb側)に大きな変更はいらない。

このように,オブジェクトを生成する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