WisdomSoft - for your serial experiences.

5.7 インタフェース

実装を持たず抽象メソッドの宣言だけを集めた特別な型をインタフェースと呼びます。インタフェースはクラスの継承関係とは無関係に実装できます。

5.7.1 実装を持たない参照型

抽象クラスは実装を持たないメソッドを宣言することによって、宣言したメソッドが将来、何らかのサブクラスによってオーバーライドされることを想定しながら仮想的にクラスを記述できる機能を提供します。これによって、データのやり取りや操作に対して実体を隠蔽することができました。これはオブジェクト指向プログラミングに極めて有効な手段ですが、これよりもさらに抽象化を進める方法があります。それがインタフェースです。

インタフェースはクラスと異なり、型だけを宣言します。つまり、新しい参照型を作成することがインタフェースの目的です。インタフェースは実装を持たない public 抽象メソッドを宣言します。抽象クラスとは異なり、実体を持つメソッドやフィールドを宣言することはできません。また、public 以外のメンバを指定することもできません。インタフェースは interface キーワードを用いて次のように宣言します。

インタフェースの宣言
インタフェース修飾子 interface インタフェース名 extends スーパーインタフェース型リスト { 本体 }

インタフェース修飾子には abstract を指定できますが、そもそもインタフェースは抽象型しか指定できないため、暗黙的に abstract になります。これは、昔の Java からの名残なので、インタフェース修飾子に abstract を指定するべきではありません。

次に interface キーワードに続いてインタフェースの名前を指定します。さらに、他のインタフェースを継承する場合は extends を使ってスーパーインタフェースを指定することができます。extends とスーパーインタフェースの指定は省略することができます。宣言構文や、継承を行えるという点は、クラスとほとんど同じであることがわかると思います。本体には、抽象メソッドを指定することができます。

インタフェースを用いる目的は様々で、上級テクニックや高度な設計論の世界ではインタフェースが上手に活用されています。インタフェースを正しく使いこなすにはそれなりの経験が必要となりますが、インタフェースそのものについては難しいものではありません。しかし最初は、インタフェースの存在意義を理解し難いかもしれません。あまりにも抽象的すぎるためです。ですが、慣れてくれば、この機能の役割に感謝するようになるにちがいありません。

インタフェース内ではメソッドを宣言することができます。メソッド宣言はクラスとほとんど同じですが、public 修飾子を持つ抽象メソッドしか宣言できないという点でクラスよりも制限されます。すなわち、暗黙的に public abstract 修飾子が指定されるのです。インタフェースのメソッド宣言で public 修飾子と abstarct 修飾子を明示的に指定することも可能ですが、これは好ましい記述ではないので避けるべきです。

抽象クラスのインスタンスを作ることができなかったのと同様に、インタフェースにもインスタンスというものは存在しません。インタフェースは単なる型情報にすぎないのです。インタフェースの役割は、インタフェースで宣言するメソッドをすべてオーバーライドしているインスタンスを、インタフェース型にワイドニング変換することによって実体のクラス型を隠蔽することにあります。抽象クラスも実体を隠蔽することができましたが、抽象クラスを継承するサブクラスでなければワイドニング変換することができませんでした。インタフェースは、サブクラスという制約を取り払うことができ、インタフェースが宣言するメソッドをオーバーライドしているクラス型であれば、確実にワイドニング変換することができます。これにクラスの継承関係は依存しません。

クラスがインタフェース型のメソッドをオーバーライドしていることを証明するには、クラス宣言で実装するインタフェースを指定します。クラスが実装するインタフェースのことをスーパーインタフェースと呼び、スーパーインタフェースを持つクラスはインタフェースを実装していると表現することができます。インタフェースを指定するには implements キーワードを用いて次のように宣言します。

インタフェースの実装
クラス修飾子 class クラス名 extends スーパークラス型 implements スーパーインタフェース型リスト { 本体 }

extends によるスーパークラスの指定を省略して、implements とインタフェースだけを指定することも可能です。Java は常に 1つのクラスしか継承できませんが、インタフェースはいくつでも実装することができます。スーパーインタフェース型リストには、カンマ , で区切って複数のインターフェイス型を指定することができます。特に、そのクラス宣言の implements で指定したインタフェースを直接のスーパーインタフェースと呼びます。

インタフェース型のリスト
implements インタフェース型, インタフェース型, ...

インタフェースを実装することによって、クラスは様々な型に変換可能になります。抽象メソッドの実装と同様に、インタフェースを実装するクラスは、必ずスーパーインタフェースで宣言されているメソッドを実装しなければなりません。そうすることによって、クラスはそのインタフェース型にワイドニング変換されても、確実にメソッドにアクセスできることを保証できるのです。インタフェースで宣言されているメソッドを実装しない場合はコンパイル・エラーとなります。

コード1
interface IVisibility {
	void show();
}

class A implements IVisibility {
	public void show() { System.out.println("A クラスの show() です"); }
}

class Test implements IVisibility {
	public static void main(String args[]) {
		IVisibility obj = new A();
		obj.show();

		obj = new Test();
		obj.show();
	}

	public void show() { System.out.println("Test クラスの show() です"); }
}
実行結果
>java Test
A クラスの show() です
Test クラスの show() です

コード1は、show() 抽象メソッドを宣言する IVisibility インタフェースを宣言しています。A クラスと Test クラスでは、それぞれがこのインタフェースを実装しています。IVisibility インタフェースを実装するクラスは、必ず show() メソッドをオーバーライドしなければなりません。

IVisibility インタフェースを実装するクラス型の参照は、IVisibility 型にワイドニング変換することができます。インタフェースはインスタンス化することはできませんが、インタフェース型の変数を宣言することはできます。インタフェース型の変数には、そのインタフェースを実装するクラスへの参照を代入することができるのです。インタフェースを実装するクラスであれば、そのインタフェースが宣言するメソッドを間違いなくオーバーライドしているため、実体のクラス型に関係なく、インタフェースから操作することができるのです。これは、継承関係では何の関連もない複数のクラスの参照を、同じ型として扱うことができるようになるという大きな可能性を秘めているのです。例えば、インタフェースの実際の使い方の 1 つとして、システムへのアクセス方法をインタフェースに統一する方法が用いられています。実体を持たないインタフェースからシステムにアクセスすれば、システムが何であれ統一した方法で制御することができるのです。システムの実体を他のシステムに取り替えても、インタフェースからのアクセス方法は変わらないため、プログラムの変更は最小限に抑えられます。これは、システムが交換可能になることを示唆しています。

優れたオブジェクト指向プログラマによる中規模以上のシステムプログラムは、必然的に「インタフェース → 抽象クラス → デフォルトシステム」という構造になる傾向があります。こうすることによって、プログラムの部品が他の部品に及ぼす影響を最小限に抑えることができ、拡張性や保守性において高い効果が期待できることを知っているのです。

オブジェクト指向言語の中には、複数のクラスを継承することができる多重継承を可能とする言語もありますが、これはシステムを複雑化させる要因となり、複雑さを隠蔽するオブジェクト指向の思想と反する結果を招くことになりかねません。そこで、Java 言語では複数の性質を持つオブジェクトはインタフェースを用いて作成します。

例えば、猫クラスは哺乳類クラスのサブクラスであるとすれば、それは同時に、脊椎動物型や夜行性型にも分類することができるかもしれません。しかし、常に 1 つのスーパークラスしか継承できない Java 言語では、複数のスーパークラスを持つことはできません。そこで、脊椎動物や夜行性といった抽象的な型をインタフェースとして実装させるのです。

interface 夜行性 {}
interface 脊椎動物 {}

abstract class 哺乳類 implements 脊椎動物 {}
class 猫 extends 哺乳類 implements 夜行性 {}
class 犬 extends 哺乳類 {}

class Test {
	public static void main(String args[]) {
		夜行性 kitty = new 猫();
		脊椎動物 puppy = new 犬();
	}
}

上記のコードは例を証明するためのジョークプログラム(よって、サンプルには収録しない)ですが有効です。猫クラスは、脊椎動物型や夜行性型に変換することができるため、問題なくこのプログラムをコンパイルすることができます。

5.7.2 定数宣言

インタフェースのメンバには、メソッドだけではなくフィールドを指定することも可能です。しかし、インタフェースの目的はインスタンスを提供することではないので、クラスのフィールドのようにインスタンスごとに割り当てることはできません。インタフェースのメンバとして宣言されたフィールドは、暗黙的に public static final 修飾子が指定された定数フィールドとなります。

インタフェースを通じてフィールドを提供する意義は、何らかの意味を持つ固定的な数値情報などをクラスやメソッド間で交換する時などに、中立的な立場から定数を提供することにあります。C 言語における列挙体のような役割をインタフェースに与えることができるのです。

コード2
interface IMessageStyle {
	int INFORMATION = 2 , WARNING = 4 , QUESTION = 8;
}

class Test implements IMessageStyle {
	public static void main(String args[]) {
		message("Kitty on your lap" , INFORMATION);
		message("Kitty on your lap" , WARNING);
		message("Kitty on your lap" , IMessageStyle.QUESTION);
	}
	public static void message(String msg , int style) {
		switch(style) {
		case INFORMATION:
			System.out.println("[一般情報] " + msg);
			break;
		case WARNING:
			System.out.println("!注意! " + msg);
			break;
		case QUESTION:
			System.out.println("?疑問? " + msg);
			break;
		}
	}
}
実行結果
>java Test
[一般情報] Kitty on your lap
!注意! Kitty on your lap
?疑問? Kitty on your lap

コード2では、定数フィールドを提供する IMessageStyle インタフェースを宣言しています。このインタフェースはメソッドを持たないため、final フィールドを提供するために存在しています。通常、こうしたインタフェースのフィールドを利用して情報交換を行うメソッドを保有するクラスは、インタフェースを実装することによってその事実をアピールします。そのクラスにとって、インタフェースを実装することによって単純名でフィールドにアクセスできるようになるというソース上のメリットもあります。

インタフェースが提供する final フィールドにアクセスするには、インタフェースを実装しない外部からは、インタフェース名を含む限定名を指定しなければなりませんが、インタフェースを実装しているクラス内では単純名でアクセスできます。これは Test クラスの main() メソッドや message() メソッドのコードから確認できます。しかし、message() メソッドの最後の呼び出しでは、限定名で IMessageStyle インタフェースの QUESTION フィールドを指定しています。インタフェースを実装していないクラスからは、このように限定名でアクセスすることができます。

Test クラスの message() メソッドは、ユーザーに何らかの文字列メッセージを通知するためのメソッドだと仮定しています。このとき、メッセージは一般情報、注意情報、疑問情報など、メッセージの種類に応じて通知方法を変化させるものだとしましょう。メッセージの種類をメソッドに与えるには boolean では不十分です。そこで、インタフェースで宣言されている数値定数を使ってメソッドにメッセージの種類を知らせているのです。こうしたメッセージの通知手法は、グラフィカルなシステムで実際に利用されています。

5.7.3 インタフェースの継承

クラスが他のクラスを継承することができるように、インタフェースもまた、他のインタフェースを継承することができます。しかし、インタフェースの継承はクラスの継承とは異なり、機能の継承ではなく概念の継承にすぎないことに注目しなければなりません。実体を持つクラスは、その機能をサブクラスに提供することができ、サブクラスはスーパークラスが持つメソッドを再利用することができました。しかし、実体のないインタフェースは抽象メソッドを継承するだけなので、インタフェースが宣言している抽象メソッドや final フィールドが増えるだけです。つまり、概念の継承です。

インタフェースを継承するには、クラスの継承と同様にインタフェース宣言で extends を指定します。インタフェースの継承関係では、基本となるインタフェースをスーパーインタフェースと呼び、継承するインタフェースをサブインタフェースと呼びます。とくに、インタフェース宣言で指定したスーパーインタフェースのことを、直接のスーパーインタフェースと呼びます。

インタフェースのメンバ宣言は、抽象的であるということ以外ではクラスのメンバ宣言と同じなので、メソッドのオーバーロードなども可能でした。では、実体を持たないインタフェースの継承関係で、オーバーライドが行われた場合はどうなるのでしょう。サブインタフェースがスーパーインタフェースのメソッドをオーバーライドすることは可能です。しかし、実体を持たないメソッドを抽象メソッドでオーバーライドすることに、あまり大きな意味はありません。

コード3
interface A { void a(); }
interface B extends A { 
	void a();	//オーバーライド
	void b();
}

class Test implements B {
	public static void main(String args[]) {
		B obj = new Test();
		obj.a();
		obj.b();
	}
	public void a() { System.out.println("A.a() メソッド"); }
	public void b() { System.out.println("B.b() メソッド"); }
}
実行結果
>java Test
A.a() メソッド
B.b() メソッド

コード3では、スーパーインタフェース A と、A を継承するサブインタフェース B を宣言しています。B は A インタフェースで宣言されているメンバを継承しているため、B インタフェースを実装する Test クラスは、A インタフェースで宣言されているメソッドも実装しなければなりません。このような場合、インタフェース B はインタフェース A に直接依存していると表現します。

ところで、B は A インタフェースの a() メソッドをオーバーライドしています。この場合、完全に同じ抽象メソッドを再定義しているにすぎないので、結果に対して何らかの意味を持つものではありません。