インターフェイス
抽象クラスは抽象メソッドと呼ばれる定義だけのメソッド(機能)を定義することが可能なクラスでした。
インターフェイスはこの抽象クラスをさらに抽象化し、publicな抽象メソッドのみで構成された抽象クラスと言えます。
また、インスタンスを作ることができない(newできない)事も共通しています。
インターフェイスは抽象クラスと似た機能です。
言語レベルでの違いは数える程度しかなく、実際の開発の中でもインターフェイスにしようか抽象クラスにしようか迷う局面はよくあります。
インターフェイスを理解する為には抽象クラスとの機能的な違いではなく、目的(用途)を知ることです。
インターフェイスの目的
抽象メソッドを使うことで共通処理を行うのであれば、実装も可能な抽象クラスで充分であり、インターフェイスは必要ないとも言えます。
しかし、インターフェイスはクラスではありません。
クラスとはプログラムの構成単位でしたが、ここで言うプログラムには仕様と実装の2つの側面があります。
仕様はどのような引数を持ちどのような処理が行われどのような結果(戻り値)を返すかを定義したもので、これはメソッドのシグニチャで表現できます。
実装はメソッドに書かれた処理(コード)でありメソッドの仕様を実現します。
クラスでは仕様と実装の両方を記述することができます。
これは抽象クラスでも同じですが、抽象クラスでは抽象メソッドとして仕様だけを定義していると言えるでしょう。
この時、実装はサブクラスで定義することが求められました。
また、メソッドはオーバーライドする事が可能ですが、これは仕様(メソッドのシグニチャ)を変えずに実装を上書きできるということです。
このように同じ仕様(メソッドのシグニチャ)であってもその実装は生成されたインスタンスのクラスに依存するのであり必ずしも同じ処理が行われるわけではありません。
変数の型は、その変数にメソッド(仕様)があるかを判断する材料になりますが、実際にどのような実装になっているかは生成されたインスタンス次第ということになります。
つまり、仕様と実装は常に1:1であるとは限らないのです。
仕様と実装の分離
アプリケーションを作っていく過程において仕様と実装を明確に分けたほうが都合がいいケースがあります。
例えば、データベースとの接続を行うクラスを作成するとします。
世の中にはOracle・DB2・MySQLなど様々なデータベースがあり、それぞれ接続方法は異なります。
異なるわけですから、それぞれのクラスを作ってしまう事も可能です。
しかし、接続するという共通のメソッドを使うことができるならば、そのクラスを使う側としてはデータベースの種類を意識することなく接続することができるでしょう。
そして、各ベンダーはそのメソッド(仕様)に合わせて実装することができます。
このように仕様と実装を分ける事は、必須ではないですが非常に有効な手段となります。
インターフェイスは仕様を実装から切り離す為の機能なのです。
実装は全てクラスに委ね、インターフェイスは公開されたメソッド(仕様)のみを定めるのです。
インターフェイス(Interface)を直訳すれば、「接合部分」「境界」「橋渡し役」といった意味であり、一般的な意味では2つのものを仲介する役割を持つ仕組みと言えます。
最も馴染み深いインターフェイスは(グラフィカル)ユーザーインターフェイス(GUI)であり、GUIはコンピュータとオペレータの間で情報を仲介しています。
Javaのインターフェイスもクラス(実装)と実行するコードの間でメソッド定義(仕様)を用いた仲介を行っています。
インターフェイスの定義
インターフェイスを作成する場合も通常のクラスと同じようにソースファイルを記述しますが、classの代わりにinterfaceキーワードを用います。
しかし、インターフェイスにはpublicな抽象メソッド以外は定義できません。
public interface Poo { public abstract void method(); }
尚、publicキーワードとabstractキーワードは省略することができます。
この省略はコンストラクタの省略などとは違い、public abstract以外はあり得ないので一般的には省略する事が奨励されています。
public interface Poo { //インターフェイスなので必ず public abstract void method(); }
インターフェイスの継承
インターフェイスはインターフェイスを継承できます。
しかし、クラスを継承することはできませんし、インターフェイスを継承してクラスを作ることもできません。
インターフェイスはインターフェイス同士で、スーパーインターフェイスとサブインターフェイスを作る事ができます。
クラスの継承と同様にインターフェイスの継承にはextendsキーワードを使います。
public interface Quu extends Poo { void method2(); }
インターフェイスの実装
抽象クラスの時はサブクラスの中で抽象メソッドを実装しました。
しかし、インターフェイスはクラスではない為、サブクラスを作ることができません。
インターフェイスを使う為にはimplementsキーワードを使います。
public class Foo implements Poo { }
このように書くことで、抽象メソッドがFooクラスに定義されます。
したがって、Fooメソッドを実装しなければ抽象クラスを継承した時と同じようにコンパイルエラーとなるのです。
public class Foo implements Poo { public void method() { // 処理 } }
このように抽象メソッドを実装することでコンパイルが成功します。
またはクラスを抽象クラスとすることでコンパイルが通ります。
public abstract class Foo implements Poo { }
この抽象クラスは、次のように書くこととほとんど同じ事です。
public abstract class Foo { public abstract void method(); }
このようにインターフェイスでは抽象メソッドを定義でき、実装(implements)したクラスに実装を強制することができます。
インターフェイス型の変数
インターフェイスはインスタンスを作ることは出来ませんが、クラスと同じように変数の型として使用することができます。
Poo poo = new Foo();
抽象クラスと同様に、Pooで定義されているメソッドのみが変数pooを通して使えるようになります。
実際に参照しているインスタンスはnewされたインスタンスであることも同じです。
インターフェイスと抽象クラスの使い分け
原則としてインターフェイスは仕様を実装から切り離す為に使用し、抽象クラスはクラスの共通部分の抽出に使います。
しかし、実際には使い分けは曖昧です。
例えば、テンプレートメソッドパターンのように一部の実装を切り離したい、というケースが多いでしょう。
完全に実装と切り離すべきだけど、一部の実装は含めて置いたほうが都合がいいケースもあります。
抽象クラスで使用したAbstractItemですが、これは価格をインスタンス変数としてもっています。
このクラスから定義(仕様)を抽出していくと次のようにインターフェイスを作成できます。
// 商品インターフェイス public interface Item { // 価格取得 int getPrice(); // 値引率(%)の取得 int getPriceOffRate(); // 値引価格の取得 int getOffPrice(); }
公開したいメソッドがインターフェイスに定義されています。
このようにクラスを作成した後にインターフェイスを抽出することも多いでしょう。
この商品インターフェイスをAbstractItemに実装させるとこうなります。
// 抽象商品クラス public abstract class AbstractItem implements Item { // 価格 private int price; // 価格設定 public void setPrice(int value) { price = value; } // 価格取得 public int getPrice() { return price; } // 値引価格の取得 public int getOffPrice() { return getPrice() * getPriceOffRate() / 100; } }
継承とインターフェイス
前のプロジェクトで使用していたRecordクラスがあったとします。
// レコードクラス public class Record { // 価格 private int price; // 価格設定 public void setPrice(int value) { price = value; } // 価格取得 public int getPrice() { return price; } }
このクラスもCalculatorクラスで計算の対象にしたいとします(Recordは割引無とする)。
CalculatorクラスにRecordクラスを引数とするメソッドを追加すれば実現はできますが、折角抽象化してメソッドをまとめたのにまた増えてしまうのです。
かといって、RecordクラスをAbstractItemを継承するように変更することも選択肢としてはありますが、どうせならば最低限の修正で対応させたいと考えます。
インターフェイスであればこの修正が簡単に元のソースをほとんど変えずに可能とします。
// レコードクラス public class Record implements Item { // 価格 private int price; // 価格設定 public void setPrice(int value) { price = value; } // 価格取得 public int getPrice() { return price; } // 追加メソッド // 値引率(%)の取得 public int getPriceOffRate() { return 0; } // 値引価格の取得 int getOffPrice() { return getPrice(); } }
既存の箇所には修正は行わず、インターフェイスで必要になった箇所のみで修正は済みました。
もし、まったくソースを修正できない状況もあります。
そのような時は、サブクラスとインターフェイスを組み合わせて次のようにすることもできます。
// レコードクラス public class NewRecord extends Record implements Item { // 値引率(%)の取得 public int getPriceOffRate() { return 0; } // 値引価格の取得 int getOffPrice() { return getPrice(); } }
継承によるメソッドの追加とインターフェイスの組み合わせ技です。
元のクラスには一切修正を行わずに抽象メソッドを追加したことに注意してください。
これによりCalculatorクラスのメソッドはItemインターフェイスに修正できます。
package example04; public class Calculator { // 合計金額 private int totalAmount = 0; // コンストラクタ public Calculator() { } // 商品を追加(商品・数量) public void addItem(Item item, int num) { int price = item.getOffPrice() * num; totalAmount = totalAmount + price; } // 商品を追加(商品・数量1) public void addItem(Item item) { addItem(item, 1); } // 合計金額取得 public int getTotalAmount() { return totalAmount; } }
Recordのインスタンスではこの計算機クラスは使うことができませんが、NewRecordのインスタンスであれば使うことが可能です。
インターフェイスを使用することにより、より抽象化が進んでいることが重要です。