Effective Javaを勉強します【第4章】
項目13. クラスとメンバーへのアクセス可能性を最小限にする
情報隠蔽のメリット
実装とAPIを分離し、他のモジュールとはAPIを介して通信を行い、詳細な実装は外部へ公開しないことを 情報隠蔽という。メリットとしては、以下のような点が挙げられる。
- 他モジュールとの依存性が低いため、モジュールの個別開発が可能であり開発スピードが向上する
- モジュールの理解がしやすく、他モジュールへの影響を心配せずデバッグが可能
- パフォーマンスに問題があるモジュールが特定できれば、他モジュールへの影響を心配せず最適化が可能
- ソフトウェアの再利用を促進する
- 大規模システムの開発において、個々のモジュールから作り込むことができる
情報隠蔽とアクセス修飾子
アクセス修飾子を用いて、各クラスやメンバーをできる限りアクセスできないようにすべき。
トップレベルのクラスやインタフェースの場合:
以下の2種の修飾子が利用可能。公開APIに含まれない(情報隠蔽可能)ので基本的にパッケージプライベートにすべき。パッケージプライベートのクラス/インタフェースを呼び出すクラスが1つならば、そのクラスにネストしてアクセス範囲を絞ることを検討すべきである。
- パッケージプライベート
- パッケージ内からのみアクセス可能
- public修飾子をつけなければパッケージプライベートになる
- public
- どこからでもアクセス可能
- 公開APIとなるため、互換性維持のために永久的にサポートする必要が生じる
メンバーの場合:
以下の4種の修飾子が利用可能。基本的にprivateにすべき。そうでないメンバーが多くなるのであれば設計を見直すべき。
- private
- 宣言されたクラス内でのみ利用可能
- 基本的にprivateにすべき
- パッケージプライベート
- 宣言されたパッケージ内のどのクラスからでもアクセス可能
- protected
- 宣言されたクラスのサブクラスからと、宣言されたパッケージ内のどのクラスからでもアクセス可能
- public
- どこからでもアクセス可能
メソッドがスーパークラスのメソッドをオーバーライドする場合、スーパークラスでメソッドが持つアクセスレベルより低いアクセス修飾子を指定することはできない。スーパークラスより低いアクセスレベルを設定すると、オーバーライドしたメソッドからスーパークラスのメソッドが呼び出せない状況が生じうるため。
インスタンスフィールドはpublic
にしない
インスタンスフィールドをfinal
で宣言した場合であっても、インスタンスフィールドへの参照のみを保持するので、インスタンスフィールド内で保存する値を制限することができないため。
同様に配列をフィールドとして持つ場合も、配列内の値ではなく配列への参照を保持するのみであるため、長さが0でない配列は常に可変であると考える。public static final
の配列フィールドやアクセッサーを持つのは、セキュリティホールのもとである。
項目14. publicのクラスでは、publicのフィールドではなく、アクセッサーメソッドを使う
public
のフィールドにはどこからでもアクセス可能となるため、カプセル化に反する。またフィールド変更時の補助的な処理も提供することができない。そのためフィールドは基本的にprivate
とし、public
のセッターとゲッターを提供するようにすればよい。
ただし、パッケージプライベートまたはprivate
のネストしたクラスであれば、public
のフィールドとしたほうが可読性などの観点から望ましい場合もある。
項目15. 可変性を最小限にする
不変オブジェクトのメリット
不変オブジェクトは以下に示すようなメリットを持つため、不必要にオブジェクトは可変とせずに不変オブジェクトの利用を検討すべきである。
不変オブジェクトは単純である:
オブジェクトが生成された状態から変化しないことが保証されるため、クライアントは状態を理解し信頼して利用することが可能である。開発者もクライアントの利用を考慮して何かする必要がない。
不変オブジェクトは本質的にスレッドセーフである:
複数のスレッドから並行してアクセスされても、状態が変化しないため同期を必要としない。すなわち制限なく共有できる。これを生かし、頻繁に利用される不変オブジェクトはpublic static final
の定数として提供したり、staticファクトリーメソッドで提供することが可能である。
また不変オブジェクトは、状態が変化しないため、コピーを行う必要がない。すなわちclone
メソッドやコピーコンストラクタを提供する必要はない。
項目16. 継承よりコンポジションを選ぶ
継承はコードを再利用するための強力な方法だが、不適切に使用されると継承は脆いソフトウェアを生み出す。以下は継承が安全に利用できる場合である。
- サブクラスとスーパークラスの実装が同じプログラマの管理下にあるようなパッケージ内で継承する場合
- 拡張のために設計されてかつ文書化されているクラスを拡張する場合
- クラスがインタフェースを実装したり、インタフェースが他のインタフェースを拡張した場合
一方、パッケージをまたがって普通の具象クラスから継承する場合は危険である。
サブクラスは適切に機能するために、スーパークラスの実装の詳細に依存する。スーパークラスの作成者が特に拡張される目的で設計および文書化を行なっていない限り、サブクラスはスーパークラスと一緒に変わっていかなければならない。
不適切な継承
不適切な継承の例として、HashSet
を使用するプログラムを以下に示す。このクラスには格納している要素の数をカウントする機能を追加している。
public class InstrumentedHashSet<E> extends HashSet<E> { private int addCount = 0; public InstrumentedHashSet() { } public InstrumentedHashSet(int initCap, float loadFactor) { super(initCap, loadFactor); } @Override public boolean add(E e) { addCount++; return super.add(e); } @Override public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return super.addAll(c); } public int getAddCount() { return addCount; } }
上記のコードは問題ないように見えるが、以下のようにaddAll
メソッドで3つの要素を格納した後、getAddCount
メソッドを呼び出すと6が返される。実際は要素数3なので、正しく動作していない。
InstrumentedHashSet<String> s = new InstrumentedHashSet<String>(); s.addAll(Arrays.asList("Snap", "Crackle", "Pop")); // 3が返ってくるのが正しいが、6が返される s.getAddCount();
これはaddAll
メソッドの内部でadd
メソッドが呼び出されているためである。add
メソッドとaddAll
メソッドの両方で要素数が二重にカウントされている。
この問題はaddAll
メソッドをオーバーライドしないことで解決することは可能である。しかし今後クラスを拡張する場合、HashSet
のaddAll
メソッドがadd
メソッドを使用しているという事実に依存することは変わらない。
また、継承したクラスで要素の追加時に何らかのバリデーションを実施するよう、オーバーライドをしたとする。この場合、実装時は問題が生じないかもしれないが、後のリリースでスーパークラスに要素を追加するための新しいメソッドが追加された場合、サブクラスでオーバーライドされていない新しいメソッドを利用することでバリデーションを回避することが可能である。
また、スーパークラスにサブクラスと同じシグネチャのメソッドが新たに追加された場合、戻り値の型が異なればサブクラスのコンパイルに失敗してしまうし、戻り値の型が同じであれば意図せずサブクラスでオーバーライドをすることで不適切な動作をするかもしれない。
要するに、パッケージを跨ったりドキュメントに記載がない意図されていない継承は、上記のような問題を引き起こしかねないため実施すべきでない。
継承における問題の回避方法
上記問題を全て回避するためには、コンポジションが有用である。コンポジションとは、既存のクラスを拡張する代わりに、既存のクラスのインスタンスを参照するprivate
のフィールドを、新たなクラスに持たせることである。
新たなクラスでは、保持している既存クラスのインスタンスに対してメソッドを呼び出し、その結果を返すようにする。これは転送と呼ばれ、そのメソッドは転送メソッドと呼ばれる。
下記はInstrumentedHashSet
をコンポジションを利用して書き換えた例である。コンポジションを利用すれば、既存のクラスを変更したとしてもそれを保持しているInstrumentedHashSet
には影響がない。
public class InstrumentedHashSet<E> extends ForwardingSet<E> { private int addCount = 0; public InstrumentedHashSet(Set<E> s) { super(s); } @Override public boolean add(E e) { addCount++; return super.add(e); } @Override public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return super.addAll(c); } public int getAddCount() { return addCount; } } // 再利用可能な転送クラス public class ForwardingSet<E> implements Set<E> { private final Set<E> set; public ForwardingSet(Set<E> set) { this.set = set; } @Override public int size() { return set.size(); } @Override public boolean isEmpty() { return set.isEmpty(); } @Override public boolean contains(Object o) { return set.contains(o); } @Override public Iterator<E> iterator() { return set.iterator(); } @Override public Object[] toArray() { return set.toArray(); } @Override public <T> T[] toArray(T[] a) { return set.toArray(a); } @Override public boolean add(E e) { return set.add(e); } @Override public boolean remove(Object o) { return set.remove(o); } @Override public boolean containsAll(Collection<?> c) { return set.containsAll(c); } @Override public boolean addAll(Collection<? extends E> c) { return set.addAll(c); } @Override public boolean retainAll(Collection<?> c) { return set.retainAll(c); } @Override public boolean removeAll(Collection<?> c) {return set.remove(c); } @Override public void clear() { set.clear(); } }
上記のクラスは頑強さに加えて柔軟性も持ち合わせている。
InstrumentedSet
クラスのコンストラクタには、以下の様にどのSet実装を渡しても問題がない。
Set<Date> s = new InstrumentedSet<Date>(new TreeSet<Date>(x)); Set<E> s2 = new InsrumentedSet<E>(new HashSet<E>(capacity));
これはInstrumentedSet
インスタンスが既存のSet
インスタンスをラップしているため、InstrumentedSet
クラスはラッパークラスとも呼ばれる。またデコレータパターンとしても知られる。
項目17. 継承のために設計および文書化する、でなければ継承を禁止する
継承されるクラスは、そのための設計及び文書化がなされていなければならない。これが具体的に何を指すかについて説明する。
オーバーライド可能なメソッドの自己利用について正確に文書化しなければならない
各メソッド/コンストラクタが、どのオーバーライド可能なメソッドをどのような順番で呼び出し、それが後の処理にどのように影響するかを正確に文書化しなければならないということ。一般的に「良いAPIドキュメンテーションは、あるメソッドが何をするのかを記述し、どのように行うかは記述すべきでない」と言われているが、安全にサブクラス化するためには例外的に許される。
賢く選択されたprotected
メソッドの形で、クラス内部の動作へのフックを提供しなければならないかもしれない
フックとは、プログラム中の特定の箇所に、利用者が独自の処理を追加できる様にする仕組み。例えばjava.util.AbstractList
のremoveRange
メソッドは、List実装の最終的なユーザーが直接利用することはないが、開発者はremoveRange
メソッドをオーバーライドすることでclear
メソッドのパフォーマンスを向上させることが可能である。
(removeRange
メソッドはclear
メソッド内から呼び出され、デフォルト実装だと2次のオーダーである。)
どのようなフックを提供すべきかを簡単に知る手段はない。フックの1つ1つは実装の詳細に対する束縛となるため、必要最小限のフックを提供できるよう、開発者は実際にサブクラスを書いてテストを行い、十分に検討する必要がある。
コンストラクタはオーバーライド可能なメソッドを呼び出してはならない
スーパークラスのコンストラクタはサブクラスのコンストラクタより前に実行されるため、サブクラスでオーバーライドしているメソッドはサブクラスのコンストラクタより先に実行されることになる。そのため、オーバーライドしているメソッドがサブクラスのコンストラクタ内の処理に依存している場合、意図しない動作をすることになる。
これと同様の問題がCloneable
インタフェースとSerializable
インタフェースを実装した場合にも生じる。そのため継承のために設計しているクラスでは、Cloneable
インタフェースとSerializable
インタフェースは実装すべきでない。
安全にサブクラス化するための設計及び文書化がされていないクラスのサブクラス化は禁止する
サブクラス化を禁止するには以下の2つの方法がある。
- クラスを
final
と宣言する - すべてのコンストラクタを
private
かパッケージプライベートにして、代わりにpublic
のstaticファクトリ〜メソッドを提供する
項目18. 抽象クラスよりインタフェースを選ぶ
インタフェースは一般的に複数の実装を許す方を定義する最善の方法である。
抽象クラスとインタフェース
Javaでは複数の実装を許すクラスを定義するために、抽象クラスとインタフェースという2つの仕組みが提供されている。違いは以下。
- 抽象クラスはメソッドの実装を含むことが許されるがインタフェースは許されない
- 抽象クラスを実装するためには抽象クラスを継承する必要あり
- インタフェースはクラス階層のどこでも実装可能
Javaでは単一継承のみが許されているため、抽象クラスの利便性を低下させている。なぜなら既に既存の抽象クラスを継承している場合、追加で新たな抽象クラスを実装することはできないため。
一方で新たなインタフェースを実装するように既存のクラスを変更することは容易である。具体的にはクラス宣言にimplements
節を追加し、必要なメソッドを実装すればよい。
ミックスイン
インタフェースはミックスインを定義するには理想的。ミックスインとは、クラスが「本来の振る舞い」に加えて、追加で何らかの任意の振る舞いを提供するために実装するクラスのこと。例えばComparable
インタフェースは、本来の機能に対して「順序付け」という振る舞いを追加するミックスインである。
前述した通り、抽象クラスは既存のクラスへ組み込むことができないため、ミックスインの定義には利用できない。
インタフェースは、階層を持たない型フレームワークの構築を可能にする。例えば歌手を表しているインタフェースとソングライターを表しているインタフェースがあると仮定する。実世界では、歌手でありソングライターであるという人もいて、その間に包含関係(階層)はない。インタフェースを用いれば、 Singer
とSongWriter
の両方を拡張したSingerSongWriter
を以下の様に定義することが可能である。
public interface Singer { AudioClip sing(Song s); } public interface Songwriter { Song compose(boolean hit); } public interface SingerSongwriter extends Singer, Songwrite { AudioClip sing(Song s); Song compose(boolean hit); }
骨格実装
インタフェースはメソッドの実装を含むことはできないが、インタフェースに対応した骨格実装クラスを提供することで、実質的にデフォルト実装を提供することが可能。
以下では無名クラスとしてList
の骨格実装(AbstractList
)が提供されている。
static List<Integer> intArrayAsList(final int[] a) { if (a == null) { throw new NullPointerException(); } return new AbstractList<Integer>() { public Integer get(int i) { return a[i]; // 自動ボクシング } @Override public Integer set(int i, Integer val) { int oldVal = a[i]; a[i] = val; // 自動アンボクシング return oldVal; // 自動ボクシング } @Override public int size() { return a.length; } }; }
補足
Java8からインタフェースがデフォルト実装を持てるようになりました。これにより骨格実装をわざわざ別のクラスに分ける必要がなくなりました。(間違っていたら教えてください。)
項目19. 型を定義するためだけにインタフェースを使用する
インタフェースは型を定義するためだけに使用すべきである。定数を提供するために使用すべきではない。
定数インタフェース
以下はインタフェースの下手な使い方の一例。
// 定数インタフェース public interface PhysicalConstants { // アボガドロ定数(1/mol) static final double AVOGADROS_NUMBER = 6.02214199e23; // ボルツマン定数(J/K) static final double BOLTZMANN_CONSTANT = 1.3806503e-23; // 電子の質量(kg) static final double ELECTRON_MASS = 9.10938188e-31; }
このような用途にはenum
型を利用するのが正しい。
定数ユーティリティクラス
enum
以外の定数の提供方法として定数ユーティリティクラスがある。定数ユーティリティクラスはインスタンス化不可能である。
public class PhysicalConstants { // インスタンス化を防ぐ private PhysicalConstants() {} // アボガドロ定数(1/mol) static final double AVOGADROS_NUMBER = 6.02214199e23; // ボルツマン定数(J/K) static final double BOLTZMANN_CONSTANT = 1.3806503e-23; // 電子の質量(kg) static final double ELECTRON_MASS = 9.10938188e-31; }
項目20. タグ付クラスよりクラス階層を選ぶ
タグ付きクラスが適切な場合は殆どない。タグ付きクラスを書こうとした時はクラス階層で表現できないか考え直すこと。
タグ付きクラス
以下はタグ付きクラスの例。
public class Figure { enum Shape { RECTANGLE, CIRCLE } // タグフィールド - 図の形 final Shape shape; // shape が RECTANGLE の場合だけ、これらのフィールドは使用される double length; double width; // shape が CIRCLE の場合だけ、これらのフィールドは使用される double radius; // 円のためのコンストラクタ Figure(double radius) { shape = Shape.CIRCLE; this.radius = radius; } // 四角のためのコンストラクタ Figure(double length, double width) { shape = Shape.RECTANGLE; this.length = length; this.width = width; } double area() { switch (shape) { case RECTANGLE: return length * width; case CIRCLE: return Math.PI * (radius * radius); default: throw new AssertionError(); } } }
上記ではShape
がタグフィールドである。タグ付きクラスはenum
宣言やタグフィールド、switch
文で可読性が低下する。またタグによっては必ずしも使用されないフィールドがインスタンスに含まれるため、メモリ量も増加する。要するに、タグ付きクラスは冗長で、誤りやすく、非効率である。
タグ付きクラスは以下のようにクラス階層で表現可能。これは上述の欠点を全て克服している。さらに新たなクラスSquare
を追加することも容易であり、柔軟である。
// タグ付きクラスに対するクラス階層の置き換え abstract class Figure { abstract double area(); } public class Circle extends Figure { final double radius; Circle(double radius) { this.radius = radius; } @Override double area() { return Math.PI * (radius * radius); } } public class Rectangle extends Figure { final double length; final double width; Rectangle(double length, double width) { this.length = length; this.width = width; } @Override double area() { return length * width; } }
項目21. 戦略を表現するために関数オブジェクトを使用する
多くのプログラミング言語では、関数ポインタ、委譲、ラムダ式などの機構を利用して、関数を受け渡すことが可能である。
例えば、Cの標準ライブラリのqsort
関数は、要素をソートするために要素を比較するコンパレータ関数へのポインタを引数で受け取る。異なるコンパレータ関数を渡すことで様々なソート順を得ることができ、これは要素のソートに関する戦略を表していると言える。(戦略パターン)
関数オブジェクトとは
Javaでは、オブジェクト参照を使用することで関数を渡すことができる。以下のように他のオブジェクトに対して何らかの操作を行うためのメソッドを1つだけ公開しているインスタンスは関数オブジェクトと呼ばれる。
class StringLengthComparator { public int compare(String s1, String s2) { return s1.length() - s2.length(); } }
具象戦略クラス
上記のStringLengthComparator
クラスのインスタンスは、文字列の長さに基づいて比較を行い順序づける、文字列比較に対する具象戦略クラスである。
具象戦略クラスの特徴として、フィールド(状態)を持たないことがあげられる。したがって全ての具象戦略クラスのインスタンスは機能的に同じであるため、生成コストを節約するためシングルトンにするのが適切である。
class StringLengthComparator { private StringLengthCoparator() {} public static final StringLengthComparator INSTANCE = new StringLengthComparator(); public int compare(String s1, String s2) { return s1.length() - s2.length(); } }
戦略インタフェース
StringLengthComparator
インスタンスをパラメータとして受け取る場合、そのパラメータの型をStringLengthComparator
とするのは不適切である。なぜならその他の具象戦略クラスのインスタンスを受け取ることができなくなるため。
このような場合は代わりにComparator
インタフェースを定義し、そのインタフェースをStringLengthComparator
が実装するようにする。この時Comparator
インタフェースを戦略インタフェースと呼ぶ。
public interface Comparator<T> { public int compare(T t1, T t2); }
無名クラスを用いた具象戦略
具象戦略クラスは、多くの場合以下のように無名クラスを使用して宣言される。しかしこの方法は、呼び出しごとに新しいインスタンスが生成されることに注意すること。もし複数回実行されるのであれば、private static final
のフィールドに保存して再利用することを検討すべき。
Array.sort(array, new Comparator<String>() { public int compare(String s1, String s2) { return s1.length() - s2.length(); } });
補足
Java8からラムダ式が導入されたので、無名クラスを使うよりも少ない記述量で関数オブジェクトを渡すことができるようになりましたね。
項目22. 非staticのメンバークラスよりstaticのメンバークラスを選ぶ
クラス内に定義されたクラスを「ネストしたクラス」と呼び、以下の4種類に分類される。
- static のメンバークラス
- 非 static のメンバークラス
- 無名クラス
- ローカルクラス
ネストしたクラスが、1つ以上のメソッドの外から見える必要がある、またはメソッド内に入れるには長すぎる場合はメンバークラスにする。メンバークラスの個々のインスタンスが、エンクロージングインスタンスへの参照が必要ならは非 static、そうでなければ static にする。
クラスがメソッド内に属しているべきであり、1箇所でのみ使用され、そのクラスを特徴付ける型がすでに存在している場合は無名クラスにする。そうでない場合はローカルクラスにする。
static のメンバークラス
エンクロージングクラスの static のメンバーであり、他の static のメンバーと同じアクセス可能性規則に従う。もし private である場合、エンクロージングクラス内からのみアクセス可能。
static のメンバークラスは、エンクロージングクラスのメンバーのすべて(private のメンバー含む)にアクセスできる。
エンクロージングクラスと一緒に使用すると有用な public のヘルパークラスとして利用されることが多い。
class Calcularor { static public enum Operation { PLUS() {...} MINUS() {...} } }
非 static のメンバークラス
static のメンバークラスとの文法的な違いは、宣言にアクセス修飾子 static があるかないかのみ。
非 static メンバークラスの個々のインスタンスは、エンクロージングクラスと暗黙に関連付けされる。具体的には以下。
- 非 static のメンバークラスのインスタンスメソッドないからエンクロージングインスタンスのメソッドの呼び出し可能
- エンクロージングインスタンスなしで、非 static のメンバークラスのインスタンスの生成不可
一般的な利用方法の1つは、以下のようなアダプターの定義手段として。
public class MySet<E> extends AbstractSet<E> { ...//大部分省略 public Iterator<E> iterator () { return new MyIterator(); } private class MyIterator implements Iterator<E>{ ... } }
エンクロージングインスタンスへアクセスする必要がないメンバークラスを宣言するのであれば、static 修飾子を常につけてstatic のメンバークラスとすべき。
private static のメンバークラスは、エンクロージングクラスが表すオブジェクトの構成要素を表現する時に一般的に利用される。
無名クラス
クラス名を持たず、エンクロージングクラスのメンバーではない。無名クラスは宣言と同時にインスタンス化される。式が許されている場所であれば、どこでも宣言可能。
ただし制限が多い。例えば、無名クラスが宣言された箇所以外で無名クラスをインスタンス化することはできない。また無名クラスのクライアントは、無名クラスがスーパータイプから継承しているメソッド以外を呼び出すことができない。
一般的な利用方法は、関数オブジェクトの生成手段として。(Java8 からはラムダ式の使用が可能となったので、利用シーンは減ったと思われる)
ローカルクラス
4種のネストしたクラスの中で、最も使用頻度が低いと思われる。ローカル変数が宣言できる場所であればどこでも宣言でき、ローカル変数と同じスコープ規則に従う。