インタフェース

クラスは主にオブジェクトの属性や振る舞いを定義する役割を果たすのに対して、インタフェースは主にオブジェクトの機能(型やAPI)を定義する役割を果たします

クラスの継承と同様にインタフェースの実装もサブタイピングを実現するために利用することができますが、クラスの継承は単一継承のため柔軟性に欠けるのに対して、インタフェースは複数実装することができ、クラスの型階層とは無関係に実装することができるため柔軟です。

Java SE8からインタフェースもデフォルトメソッドを持てるようになったため、抽象クラスとの相違が少なくなりました。この章では 抽象クラスとの違いについても見ていきます。

この章ではインタフェースの基本的な内容についてまとめています。

 

インタフェースの定義

インタフェースの定義

インタフェースは「interface」を使って定義します。フィールドやメソッドの定義の仕方はクラスと同じです。尚、インスタンスを生成できないのでコンストラクタは定義できません。

インタフェースを実装するクラスを定義するには、「implements」に続いて実装するインタフェースを指定します。

複数のインタフェースを実装する場合は、「implements」に続いて 実装するインタフェースを全て列挙します。

インタフェースはクラスと異なり 多重継承が可能です。Java SE8以前はインタフェースはメソッドの実体を持つことができなかったため、多重継承特有のダイヤモンド問題は皆無でした。しかしJava SE8からインタフェースもメソッドの実体を持てるようになり、ダイヤモンド問題を考慮する必要がでてきました。(ダイヤモンド問題については後述します。)

 

修飾子

インタフェースの抽象メソッド(実体を持つデフォルトメソッド以外)には 暗黙に「public」と「abstract」の修飾子がつけられます。(クラスの場合はアクセス修飾子を省略した場合はパッケージプライベートとなり アクセスレベルが異なることに注意が必要です。)

また、フィールドには暗黙に「public」、「static」、「final」の修飾子が付けられます。(つまり変更可能なフィールドやインスタンスフィールドを持つことができません。)

暗黙に付与されるインタフェースの修飾子については、「冗長だから省略せよ」と主張する人もいれば「誤解の無いように省略するな」と主張する人もいて意見が分かれるところです。開発現場で方針を統一するのが良いと思います。

 

Javaインターフェース」(ひしだま’s ホームページ)

 

インタフェースの誤用(定数インタフェース)

インタフェースは型と機能を定義するために利用します。しかし、定数をまとめて定義するような場合に インタフェースが利用されるケースをときどき見かけます。そのような定数だけを定義したインタフェースは定数インタフェースと呼ばれます。Java標準ライブラリにも定数インタフェースが存在します。(java.io.ObjectStreamConstantsなど)

確かにインタフェースは(具象)クラスと違ってnewでインスタンス化することはできず、フィールドはpublic、static、finalな物に限られるため、定数をまとめて定義する場所としてうってつけに思えるかも知れません。そのような定数インタフェースを いかなるクラスも実装しないのであれば 問題はありません。しかし、定数インタフェースを実装するクラスの作成を禁止することはできませんし、そもそもインタフェースはクラスが実装するための物なので 実装を禁止すること自体本末転倒です。そうして 定数インタフェースを実装したクラスが作成されると、あちこちのクラスに同じ定数が存在することになってしまいます。そのため、定数定義をまとめるために 定数インタフェースを用意するのは 良い方法ではありません

定数を外部に提供したい場合は次のようにします。定数がクラスやインタフェースに強く関連しているものであれば、そのクラスやインタフェースの定数として定義します。そうでない定数をまとめて定義したいような場合は、定数インタフェースの代わりにユーティリティクラスを定義します。ユーティリティクラスは インスタンス化を禁止していて、定数定義と便利なクラスメソッドを提供するクラスです。コンストラクタをprivateにしてstaticファクトリメソッドなどを提供しないことによって インスタンス化を禁止することができます。

 

デフォルトメソッドとクラスメソッド(Java SE8~)

デフォルトメソッド

Java SE8から インタフェースに実体を持ったメソッド(デフォルトメソッド)を定義することができるようになりました。従来はインタフェースをバージョンアップして抽象メソッドを追加した場合、該当インタフェースを実装しているクラス全てに変更の義務が発生するため、実装クラスの変更なしには インタフェースに抽象メソッドを追加することはできませんでした。しかし、デフォルトメソッドの登場で インタフェース側で差分を吸収することができるようになり、実装クラスに影響を与えることなく インタフェースの抽象メソッドを追加できるようになりました。

とは言え、全ての実装クラスの要件を満たすデフォルトメソッドを書けない場合もあるため注意が必要ですコンパイルは通っても 実行時に失敗したり、要件を満たさなかったりする場合があります。一つの例が Java SE8でCollectionに追加されたremoveIf()メソッドです。多くのCollectionの実装クラスはデフォルトメソッドの内容で事足りますが、同期Collection(Collections.synchronizedCollection()が返すCollectionの実装クラス)では デフォルトメソッドでは排他制御が行われないため、removeIf()メソッドをオーバーライドする必要がありました。この例からも分かるとおり、デフォルトメソッドを追加した場合、実装クラスを変更しないといけない場合も出てくるため、後からデフォルトメソッドを追加することはリスクを伴います

メソッドをデフォルトメソッドにするには メソッド宣言に「default」を指定します。

 

クラスメソッド

Java SE8から インタフェースにクラスメソッドを定義することができるようになりました。それまでは インタフェースに関連するユーティリティメソッドはコンパニオンクラスに実装するしか方法がありませんでした。例えばCollectionに関連するユーティリティメソッドは Collectionsクラスに実装されていました。

インタフェースにクラスメソッドを定義できるようになったため、コンパニオンクラスを用意しなくても インタフェース自身にユーティリティメソッドを実装することができるようになりました。

 

サブタイピング

サブタイピングを実現する2つの手段

Javaにおいてサブタイピングを実現する手段には クラスの継承インタフェースの実装があります。クラスの継承ではスーパークラスがスーパータイプ、サブクラスがサブタイプになります。インタフェースの実装ではインタフェースがスーパータイプ、インタフェースを実装するクラスがサブタイプになります。

オブジェクト指向プログラミング」の章の「サブタイピング」で説明した通り、リスコフの置換原則により スーパータイプのオブジェクトが使われている箇所は全てサブタイプのオブジェクトで置換可能になります。

クラスの継承によるサブタイピングとインタフェースの実装によるサブタイピングは次の点が大きく異なります。

  1. クラスは単一継承ですが、インタフェースは複数実装できます。言い換えると、サブクラスは1つのスーパークラスのサブタイプであるのに対して、インタフェースを実装したクラスは複数のインタフェースのサブタイプになれます。言い換えると、複数の型とみなせることができ、インタフェースの重要な側面です。
  2. 既存のクラスに新たにインタフェースを実装することは容易に行えます。それに対して既存のクラスを新たなクラスのサブクラスとすることは容易には行えず不可能な場合もあります。(既存のクラスがObject以外のクラスから派生している場合は不可能です。)

インタフェースの実装では 必要な型(機能)を必要なだけ取り込むことができ、後から追加できる柔軟性も持っています。サブタイピングにおいて 型を定義する手段としては、クラスよりもインタフェースの方が適していると言えます。

型を指定する場面ではインタフェースを用いる

前述の通り、メソッドの引数・戻り値・ローカル変数・フィールドなど 型を指定する場面では、適切なインタフェースがあれば インタフェースを指定した方が柔軟性を上げることができます。唯一インタフェースでは指定できず 具象クラスでないと指定できないのは、コンストラクタでインスタンスを生成する場合です。

クラスによっては 型をインタフェースではなく抽象クラスで定義しているものもあります。java.ioパッケージのXXXInputStream、XXXOutputStream、XXXReader、XXXWriterクラスなどです。そういったクラスの場合はインタフェースの代わりに抽象クラスで型を指定することで柔軟性を上げることができます。

また、インタフェースで定義されておらず 実装クラスで定義しているメソッドを呼び出したい場合は その実装クラスにキャストすることになります。例えばArrayListにはtrimToSize()というArrayListの容量を現在のリストのサイズにまで減らすメソッドを提供していますが、これはListインタフェースには定義されていないメソッドです。このメソッドを呼び出したいときは ArrayListクラスにキャストして呼び出す必要があります。

 

スケルトン実装

Javaの標準ライブラリを眺めると ComparableやAutoCloseableのように抽象メソッドが僅かしか定義されていないインタフェースもあれば、ListやMapのように抽象メソッドが沢山定義されているインタフェースもあることが分かります。

抽象メソッドが沢山定義されているインタフェースを実装する場合、それらの抽象メソッドを全て実装する必要があり、それなりのコーディング量を必要とします。しかし、中には デフォルトの振る舞いを規定できるメソッドもあります。そのようなメソッドについては、予め実装を提供しておくと インタフェースの実装者が効率良く開発を行うことができます。このような実装補助をスケルトン実装と呼びます

スケルトン実装を提供する手段として次の2つの手段があります。

  • インタフェースを実装した抽象クラス(スケルトン実装クラス)を用意する
  • インタフェースでデフォルトメソッドを実装する

スケルトン実装は 基本となる他のメソッドを利用することにより振る舞いを規定できる場合が多いです。例えばCollectionのスケルトン実装クラスであるAbstractCollectionでは、iterator()とsize()の2つを除く全てのメソッドは、iterator()とsize()を利用することによりデフォルトの実装を規定しています。(但しAbstractCollectionは変更不可なコレクションの抽象クラスなため、add()はUnsupportedOperationExceptionを投げるだけになっています。)AbstractCollectionを継承してiterator()とsize()を実装するだけで 完全に動作する変更不可なコレクションクラスを作成することができます。

次の節では 同じスケルトン実装でも 抽象クラスとインタフェースのデフォルトメソッドでできることの違いを見てみます。続いて スケルトン実装の提供と スケルトン実装クラスの利用方法を見てみます。

 

抽象クラスとインタフェース

Java SE8からインタフェースのメソッドがデフォルト実装を持つことができるようになったため、抽象クラスとインタフェースの相違が少なくなりました。抽象クラスとインタフェースのデフォルトメソッドは どちらもメソッドの実体を持つことができますが、デフォルトメソッドには次のような制限があります。

  • 抽象クラスはインスタンスフィールドを持つことができますが、インタフェースはインスタンスフィールドを持つことができません。したがって、インスタンスフィールドを使用するようなデフォルトメソッドを書くことはできません
  • インタフェースのデフォルトメソッドでは、Object由来のメソッド(equals()、hashCode()など)を実装することができません

この制限に引っ掛かる場合は、必然的に抽象クラスによるスケルトン実装クラスを作成することになります。

また、次のような場合もスケルトン実装クラスを作成することになります。

  • インタフェースの基本的な実装が複数考えられる場合
    例えば同じListインタフェースを実装するクラスでも、内部データを 配列のようなランダムアクセス・データストアとするのか、リンクリストのような順次アクセス・データストアとするのかで実装が異なってきます。そのような場合は どちらか想定してListインタフェースのデフォルトメソッドを実装するよりは、それぞれのスケルトン実装クラスを提供する方が適切でしょう。実際にJava標準ライブラリでは、ランダムアクセス・データストア用のスケルトン実装クラスAbstractArrayListと順次アクセス・データストア用のスケルトン実装クラスAbstractSequentialListが用意されています。
  • 自分が管理していない既存のインタフェースのスケルトン実装を用意する場合
    他人が作成したインタフェースにデフォルトメソッドを追加することはできないため、この場合もスケルトン実装クラスを作成することになります。

インタフェースの利用者が スケルトン実装クラスが提供されていることに気付かない可能性も考えられるため、可能であれば デフォルトメソッドとして提供するのが良いでしょう。そして上述のようなデフォルトメソッドでは対応できないような場合に スケルトン実装クラスを提供するのが良いと考えられます。

 

スケルトン実装の提供と利用方法

スケルトン実装の提供

インタフェースを公開する際には 次のようにスケルトン実装の提供を検討します。

インタフェースのメソッドのうち、基本的な実装が明らかなメソッドがあれば 実装補助を検討します。特に基本となる他のメソッドを呼び出すことで振る舞いを規定できるメソッドについては 全て実装補助を提供します。そのようなメソッドについては、まずデフォルトメソッドでのスケルトン実装を検討します。前節で挙げたようなデフォルトメソッドでは対応できないような場合には スケルトン実装クラスの作成を検討します。

スケルトン実装クラスは 抽象クラスである必要はありません。その場合は単純実装クラスになり、利用者は単純実装をそのまま使うこともできますし、拡張して使うこともできます。

 

スケルトン実装クラスの利用方法

スケルトン実装が抽象クラスとして提供されている場合の利用方法を見てみます。

インタフェースの利用者は スケルトン実装クラスを利用することもできますし、スケルトン実装クラスを利用せずに1からインタフェースの全てのメソッドを実装することもできます。自由に選択できますが、大抵 スケルトン実装クラスを利用した方が効率的です。スケルトン実装クラスの利用方法は2通りあります。

  • スケルトン実装クラスを継承したサブクラスを作成する
    サブクラスを作成して必要なメソッドを実装したりオーバーライドします。匿名クラスとして アダプタを作成するような場合に利用することもできます。
  • コンポジションでスケルトン実装クラスをラッピングする
    クラス」の章の「継承に代わるコード再利用のパターン:コンポジション」で取り上げたコンポジションを適用することができます。スケルトン実装クラスをコンポジションにおける再利用元クラスにして、インタフェースのメソッドを転送するようにします。このパターンは スケルトン実装クラスとは別のクラスのサブクラスにしないといけないような場合に適用することができます。

 

インタフェースの継承

あるインタフェースを継承して別のインタフェースを定義する場合 クラスと同様に「extends」でスーパーインタフェースを指定します。

インタフェースは多重継承が可能です。多重継承にまつわるダイヤモンド問題については 次の章で説明します。

 

 

多重継承のダイヤモンド問題

インタフェースは多重継承ができますが 以前はメソッドの実体を持つことができなかったため、多重継承特有のダイヤモンド問題とは無縁でした。しかしJava SE8からデフォルトメソッドを持てるようになったことから ダイヤモンド問題を考慮する必要がでてきました。

ここではダイヤモンド継承時のルールを整理します。

クラスのメソッドの実装を優先

インタフェースとスーパークラスで それぞれ同じシグニチャのメソッドの実体を持つ場合、サブクラスで該当メソッドを呼び出すとスーパークラスのメソッドが呼び出されます。(クラスのメソッドがインタフェースのメソッドより優先されます。)

 

 

継承関係で最も下のインタフェースの実装を優先

継承関係のあるインタフェースがそれぞれデフォルトメソッドの実体を持つ場合、それらのインタフェースを実装したクラスのメソッドを呼ぶと 継承関係で最も下に位置するインタフェースのメソッドが呼び出されます

 

メソッド参照を解決できない例

クラスが実装する複数のインタフェースの間に継承関係が無い場合や、継承関係があっても同じ階層の場合は前述のルールが当てはまらず メソッド参照を解決することができません。(コンパイルエラーとなります。)

このような場合は、次の例のように実装クラス(SomeClass)でデフォルトメソッドをオーバーライドすれば 参照を解決できない問題を解消することができます。インタフェースのデフォルトメソッドを呼び出す場合は「インタフェース名.super.メソッド名」で参照することができます。

 

superで参照できる範囲

「インタフェース名.super.メソッド名」で参照できるのは クラスの場合は 直接実装しているインタフェースのみになります。また、インタフェースの場合は 直接の親であるスーパーインタフェースのみになります。

 

 

特殊なインタフェース

マーカインタフェース

マーカインタフェースは フィールドやメソッドを一切持たないインタフェースで、マーカインタフェースを実装することによって何らかの特性を持つことを示すために使います。

例えば Cloneableインタフェースを実装しているクラスは clone可能であることを示し、Serializableインタフェースを実装しているクラスは インスタンスがシリアライズ可能であることを示します。

 

関数型インタフェース(Java SE8~)

Java SE8からラムダ式の導入により 関数型言語の機能や特徴を取り入れることになりました。これによって関数オブジェクトを変数に代入したり メソッドの引数や戻り値として渡すことができるようになりましたが、Javaは型を明確に定義する言語であるため 関数の型を表す手段が必要になりました。そこで関数の型を表す手段として 既存のインタフェースを利用することになり 一定の条件を満たすインタフェースを関数の型を表すためのものとし 関数型インタフェースと名付けられました

関数型インタフェースは抽象メソッドを1つだけ持つインタフェースで、その抽象メソッドが関数の型(引数、戻り値)を表します。関数型インタフェースは抽象メソッドは1つしか持てませんが、他にクラスメソッドやデフォルトメソッドをいくつ持っていても構いません。

関数型インタフェースで型付けされた変数にはラムダ式・メソッド参照・コンストラクタ参照を渡すことができます。

独自に関数型インタフェースを定義する場合は インタフェースの宣言に @FunctionalInterfaceアノテーションをつけると コンパイラが関数インタフェースであるかどうかチェックをしてくれるので便利です。

 

 

タイトルとURLをコピーしました