オブジェクト指向においてインターフェースは重要な役割を果たしています。
しかし、「インターフェースは実装がない、メソッドの定義だけをするものだ!」という実装上のことだけを教わっても、その意義を理解していないとその重要さを実感することはできません。
そこで、今回はJavaのCollectionを具体例にして、インターフェースというものの意義についてまとめていきたいと思います。
インターフェースの意義
結論から言うと、インターフェースとは「役割」を表すものです。
言い換えると、そのインターフェースがどのような「ふるまい」をするかということを表すものです。
もっと具体的に~Collectionインターフェース~
では、Collectionインターフェースについて見てみましょう。
Collection
コレクション階層のルート・インタフェースです。コレクションは、その要素であるオブジェクトのグループを表します。コレクションによっては要素の重複を許可しますが、許可しないコレクションもあります。また、順序付けられているコレクションとそうでないコレクションがあります。
つまり、Collectionとは特定のオブジェクトのグループという「役割」を持ったインターフェースだということです。
そして、Collection自体は重複や順序付けの可否については規定しないということになっています。
Collectionの継承先~SetとList~
今度はCollectionを継承したインターフェースであるSetとListについて見てみます。
Collectionよりもこちらのほうが馴染みがあるという人は多いのではないでしょうか?
Set
重複要素のないコレクションです。すなわち、セットは、e1.equals(e2)であるe1とe2の要素ペアは持たず、null要素を最大1つしか持ちません。その名前が示すように、このインタフェースは、数学で言う集合の抽象化をモデル化します。
List
順序付けられたコレクションです。シーケンスとも呼ばれます。このインタフェースのユーザーは、リスト内のどこに各要素が挿入されるかを精密に制御できます。ユーザーは整数値のインデックス(リスト内の位置)によって要素にアクセスしたり、リスト内の要素を検索したりできます。
セットとは異なり、通常、リストは重複する要素を許可します。
これらを表にまとめると、
役割 | 重複 | 順序 | |
---|---|---|---|
Collection | 要素のグループ | ? | ? |
Set | 要素の集合 | × | ? |
List | 順序付けられた要素のグループ | ○ | ○ |
SetとListはCollectionクラスを継承しているので、より具体的な役割を持つインターフェースになっていることが見て取れると思います。
インターフェースのメソッド
ここで、各インターフェースのメソッドに目を向けてみます(定義などは上に挙げたJavaDocを参考にしてください)。
Collectionには、size(要素の数)やremove(要素の削除)などのグループに対する操作が規定されています。
Setも基本的にはCollectionにあるようなメソッドが定義されていますが、ListではindexOf(指定要素のインデックスの検索)といったインデックスに関するメソッドが定義されています。
Setにはそもそもインデックスの概念がない(順序付けを行わない)ので、インデックス系の処理はListという役割に帰属する処理だということです。
また、addというメソッドに着目すると、Collectionで定義したメソッドをSetやListでオーバーライドしており、それぞれでメソッドの説明が異なっています。
インターフェース | addの説明 |
---|---|
Collection | 指定された要素がこのコレクションに格納されていることを保証します。 |
Set | 指定された要素がセット内になかった場合、セットに追加します。 |
List | 指定された要素をこのリストの最後に追加します。 |
Collectionではaddは追加処理ではなく、格納の保証となっています。
これは、Setのように追加しようとした要素が既にある場合は追加されないという役割のものがあるため、Collectionという役割から見たときにaddの処理は「要素追加されるときもあれば追加されないときもある。でも、少なくともaddの実行後は追加しようとした要素はグループ内に存在しているということを保証するよ」という意図を表していることになります。
SetやListのaddでは、それぞれもっと具体的な内容になっていますね。
これは、それぞれが継承によってより具体的な「インターフェースの役割」を持ったことで「メソッドの役割」もより具体的になっているということです。
実装上はインターフェースのメソッドのオーバーライドはあまり意味を持ちませんが、「そのメソッドがどのインターフェースに帰属するか」という意図を明示的に表現するためにオーバーライドしていると言えます。
役割?なにそれ?おいしいの?
インターフェースやそのメソッドがそれぞれ何らかの「役割」を表現するものであることはわかっていただけたかと思います。
しかし、「なぜ役割を表現することが大事なのか?」というところが疑問となってきます。
その説明をする前に、2つのコードを見てもらいましょう。
ArrayList<String> list = new ArrayList<String>();
list.add("abc");
List<String> list = new ArrayList<String>();
list.add("abc");
このようなコードは見たことがある人も多いと思います。
普通は下のような書き方が推奨されています。
なぜかというと、下のコードの方が「より抽象的な概念に依存した処理」だからです。
なぜ具体的じゃダメなんですか?
なぜ、抽象的なものに依存するようにコードを書くかというと、インターフェースの実装クラスとそれを使うクラスの処理を分離するためです。
例えば、Listの実装クラスをArrayListを使っていた処理を後からLinkedListに変えたいということがあったとします。
その場合、先の2つのコードはそれぞれ次のようになります。
LinkedList<String> list = new LinkedList<String>();
list.add("abc");
List<String> list = new LinkedList<String>();
list.add("abc");
この2つのコード、一見どちらも1行目を変わっただけのように思えます。
しかし、コードの意図という観点から2行目を考えてみると下のコードは「Listにaddする」という意図のままであるのに対し、上のコードは「ArrayListにaddする」から「LinkedListにaddする」という意図に変化しています。
そのため、実装クラスの変更に伴ってコードの意図が変化していないかということを確認する必要が出てきます。
実装クラスを自作する場合、これがさらに大きな効用をもたらすことになります。
Listインターフェースの役割に一致さえしていれば、どのような内部実装を行っても問題ないことになります。
使う側から見ると「Listインターフェースの実装クラスはその役割を満たすような実装を行っている」という保証の下で処理を書いていくことができるということです。
これを突き詰めていくと、使う側はインターフェースにだけ依存していれば、実装クラスがなくとも処理を書くことができるということです。
まさに、「使う側と実装クラスの分離」ですね。
まとめ
いろいろ見てきて、インターフェースの意義をつかめたのではないかと思います。
しかし、これらの考えは実装クラスがインターフェースの役割に従って実装され、その役割を利用して使う側の処理が記述されるという前提に基づいています。
そのため、各インターフェースの役割を意識して実装していくということが大切ですし、役割を意識してインターフェースを自作していくことで恩恵を最大限に受けることができます。