オブジェクト指向 Advent Calendar が空いていたので,飛び入りします!
が, Java の文法はなんとなく知っているけど OOP は身についていないビギナー向けです.拙い解説かもしれませんが何かのお役に立てたら幸いです.
Introduction
新人に Java を教える中で,Java Collection API を扱うようになると,自分なりに調べた新人君が書いた以下の様なコードを,よく見ます.(この記事を書こうと思った日は,1日で3回見ました...)
ArrayList<String> values = new ArrayList<String>();
これは書いては行けないコードで,正しくは
List<String> values = new ArrayList<String>();
と書くべきなのですが,その理由を順を追って説明したい,というのがこの記事の目的です.
interface は「仕様」
Oracle の Java SE Documentation で,java.util.List のページを見てみると,List
は interface として定義されています.
つまり,List
に定義されている add(E e)
や clear()
や get(int index)
といったメソッドは,定義のみが宣言されており,具体的な処理の手順は記述されていません.
上述した以外のメソッドを端折ると,以下のようになっています.
public interface List<E> {
boolean add(E e);
void clear();
E get(int n);
}
interface が存在する理由をやや乱暴に説明すると, 機能の定義を提供すること です.List の例で言えば「List<E>
は重複を許容し順序性を担保した E 型のオブジェクトの集合である」ことや,「List<E>
は,その順序の末尾に要素を追加することができ.追加に成功したかどうかを確認できる」とか,「集合に属する要素をすべて消し去ることが出来る」とか,「順序の n 番目の要素を指定して取得することができる」とかです.
この「機能の定義」のことを, 仕様 と呼んだりします.
逆に,interface が提供していないのは何かというと, 仕様をどのように実現しているか です.
interface は「どのように実現しているかは知らないけれど このような機能を提供するよ」ということを「約束だけ」して,「実際に約束を守る」役割は果たしません.
実装クラスは「仕様の実現」
では,実際に List
に対して add
や clear
や get
メソッドを呼んだ場合に動いている処理はどこにあるのか?それが,冒頭の例でいうところの ArrayList<E>
です.
実際のソースコードは,諸々を端折ると,以下のようになっているはずです.
public class ArrayList<E> implements List<E> {
/* いろいろなフィールド変数の定義 */
@Override
public boolean add(E e) {
/* 具体的な処理と,最後に return 文. */
}
@Override
public void clear() {
/* 具体的な処理 */
}
@Override
public E get(int i) {
/* 具体的な処理と,最後に return 文 */
}
}
ArrayList
は,内部でフィールド変数として保持している配列の要素を加えたり除いたりして,List
の順序性や重複の許容を持った集合を実現しています.このように, 仕様を何らかの方法で実現することを「実装する」 といいます .
今回の例で言うと「ArrayList
は, List
が記述している機能の定義を,配列を使って実現している」わけで,これを「ArrayList
は List
を実装している」といいます.
実際に List
の実装は ArrayList
以外にも無数に存在します.Java 標準ライブラリに含まれている LinkedList
や,Google 製ライブラリである Guava が独自に提供する変更不可能な ImmutableList
などがよく使われているでしょう.
密結合の例: やってはいけない
ありきたりですが,人をモデル化した Person
クラスを定義し,人は文字列型の名前と,文字列の ArrayList
型で複数の関心事を持つとしましょう.
関心事をすべて上書きするメソッドと,すべての関心事を取得するメソッドも追加します.
class Person {
private String name;
private ArrayList<String> interests; // ① フィールドの宣言を ArrayList にしている.
Person(String name) {
this.name = name;
this.interests = new ArrayList<>();
}
/** これまでの関心事をすべて,メソッドの引数で与えられたもので上書きする */
void refleshInterests(ArrayList<String> newInterests) {
this.interests = newInterests; // ② : 右辺も左辺も ArrayList 型
}
/** この Person の関心事をすべて返します. */
ArrayList<String> getAllInterests() {
return this.interests; // ③ : メソッドの返り値も interests も ArrayList 型
}
}
この Person
クラスを使うコードは以下のように書けます.
Person p = new Person("Hikaru Ijuin");
ArrayList<String> newInterests = new ArrayList<>();
newInterests.add("テレビ制作");
newInterests.add("芸人野球");
newInterests.add("toto BIG");
p.refleshInterests(newInterests); // ④ : 引数は ArrayList
ArrayList<String> interests = p.getAllInterests(); // ⑤ : ArrayList 型で受けられる.
この Person クラスは,以下の様なケースで困ってしまいます.
- Person の interests 変数は
ArrayList
よりLinkedList
の方がパフォーマンスが良いと判明したとしましょう.
しかし,① の行の変数宣言をLinkedList
に変えると,② も ③ もコンパイルが通らなくなってしまうので,refleshInterests
メソッドもgetAllInterests
メソッドも型をArrayList
からLinkedList
に変更しなければいけません.すると,④ も ⑤ もコンパイルが通らなくなってしまいます. -
Person
クラスを使う コードが,何らかの理由でnewInterests
やinterests
をLinkedList
として扱いたかったとしても,一度ArrayList
との変換を挟む必要が生じます.なぜならArrayList
とLinkedList
は全く別の型だからです.具体的に,以下のコードはコンパイル出来ません.
// 左辺は LinkedList, 右辺は ArrayList なのでコンパイルエラー
LinkedList<String> interests2 = p.getAllInterests();
LinkedList<String> newInterests2 = new LinkedList<>();
/* newInterests2 に値を詰める処理は省略 */
// メソッドの引数の型は ArrayList, newInterests2 の型は LinkedList なのでコンパイルエラー
p.refleshInterests(newInterests2);
なぜこのような問題が起きるのでしょうか.getAllInterest()
メソッドで言えば, ** メソッドはクラスの外側に対して「順序付けられた集合を返す」というメソッドの「仕様」を表明しているべきなのに,上記の例では「配列に値を保持することで順序付けられた集合を返す」という具体的な「実装」までを決めてしまっている** ことが原因です.もちろん refleshInterests
も同様の問題を抱えています.
上記の利用例は Person
の仕様 だけでなく 実装にまで依存してしまっている,と言えます.このような状態を 密結合 といいます.
疎結合の例: 仕様と実装を分ける
密結合な状態を解消するには,Person
クラスと,それを使うコードを,以下のように変更すればよいでしょう.
class Person {
private String name;
// 本当はこれも List で宣言するべきだが,例のためそのままにする.
private ArrayList<String> interests;
Person(String name) {
this.name = name;
this.interests = new ArrayList<>();
}
// List 型ならなんでも受け入れられるようにする
void refleshInterests(List<String> newInterests) {
this.interests = new ArrayList<>();
this.interests.addAll(newInterests);
}
/** 具体的にどのような List が返ってくるか,クラスの外側からはわからなくなった */
List<String> getAllInterests() {
return this.interests;
}
}
Person p = new Person("Yuji Tanaka");
LinkedList<String> newInterests = new LinkedList<>();
newInterests.add("猫");
newInterests.add("巨人");
newInterests.add("子供");
p.refleshInterests(newInterests); // 引数は List の実装なら何でも良い
List<String> interests = p.getAllInterests(); // 具体的な List の実装はわからない
上記のように改善された Person
型を使おうとすると,「Person
がどのように実装されているか」を知ることもないし,例えば Person
クラスのみがバージョンアップして,内部で LinkedList
で値を保持するようになっても,そのメソッドを呼び出すコードは変える必要がなくなりました.
Person
クラスとそれを使うコードの間は「どのような仕様のオブジェクトをやり取りするか」だけで繋がっており,やり取りされるオブジェクトの具体的な実装は知らないで済んでいます.このような関係を 疎結合 といいます.また,Person は interests
の具体的な実装を呼び出す側のコードから隠しています.このように実装を他のコードから隠すことを カプセル化 と呼びます.
クラスが実装をカプセル化することで,そのクラスの提供するメソッドの「仕様」を保ちつつ,「実装」は後のバージョンアップで変更することを可能とするというメリットを享受できます.(もちろん,それ以外にもメリットは有るのですが.)
まとめ
最初の話題に戻りましょう.
ArrayList<String> values = new ArrayLsit<String>();
と書かずに
List<String> values = new ArrayList<String>();
と書くべきなのも,もう納得でしょう.
変数の宣言を行う際は,後にその変数を利用する際に着目されるのは「どのように実装されているか」ではなく「どのような仕様のオブジェクトが提供されるか」です.だから,具体的な実装ではなく抽象的なインターフェイスで宣言する.
これによって,具体的な実装を切り替えたくなった時に,変更するべき箇所を最小限に抑えることが出来る,というわけでした.
もちろん,実際に具体的な実装のクラスで変数を宣言する必要が生じる場合もあります.それは具体的な実装が提供する固有の機能への依存が存在する場合です.その違いを見分けるためにもこのような背景を知っている必要があるでしょう.