Effective Java 3rd [Chapter 4] - クラスとインタフェース
クラスとインタフェース
Effective Java 第 3 版の個人的メモ
- 項目 15 クラスとメンバーへのアクセス可能性を最小限にする
- 項目 16 public のクラスでは、public のフィールドではなく、アクセッサーメソッドを使う
- 項目 17 可変性を最小限にする
- 項目 18 継承よりコンポジションを選ぶ
- 項目 19 継承のために設計および文書化する、でなければ継承を禁止する
- 項目 20 抽象クラスよりインタフェースを選ぶ
- 項目 21 将来のためにインタフェースを選ぶ
- 項目 22 型を定義するためだけにインタフェースを使う
- 項目 23 タグ付クラスよりクラス階層を選ぶ
- 項目 24 非 static のメンバークラスより static のメンバークラスを選ぶ
- 項目 25 ソースファイルを単一のトップレベルのクラスに限定する
- 参考 URL
1. 項目 15 クラスとメンバーへのアクセス可能性を最小限にする
1.1. 結論
- 常にアクセス可能性をできる限り小さくするべき
- public のクラスは public のフィールドを持つべきではない
- public static final のフィールドが配列の場合、その配列の内部も不変であることを保証するべき
1.2. 常にアクセス可能性をできる限り小さくするべき
情報秘匿(カプセル化)をすると以下の利点が得られるから。
- モジュールを分離し、個別に開発、テストなどができる
- モジュールを素早く理解できる
- ソフトウェアの再利用をするための必要条件
1.2.1. カプセル化の方法
- アクセス修飾子
- private
- パッケージプライベート
- protected
- public
- protected 以上は API になることが多い。
- パッケージで分ける。
- モジュールシステムを利用する。
モジュールシステムは、このような記述をし、以下を定義するために使用するもの。
・ どのパッケージを外部から見えるようにするか
・ 他のどのモジュールに依存するか
|
|
1.3. public のクラスは public のフィールドを持つべきではない
スレッドセーフにできないから。
メソッドにはsynchronized
が付けれるが、フィールド値にはsynchronized
を付けれないのでgetter
/setter
を利用するようにしましょう。
public static final のフィールドは定数として使ってよい
1.4. public static final のフィールドが配列の場合、参照されるオブジェクトが不変であることを保証するべき
以下のようなコードを記載すると、VALUES
のアドレスはfinal
であるが、要素はfinal
ではない。
|
|
1.4.1. 実装方法
- 方法 1
- 定数にしたい配列を private にして、public static final のリストに追加する
|
|
- 方法 2
- 定数にしたい配列を private にして、public メソッドで配列のコピーを返す
1 2 3 4
private static final Thing[] PRIVATE_VALUES = { ... }; public static final Thing[] values() { return PRIVATE_VALUES.clone(); }
- 定数にしたい配列を private にして、public メソッドで配列のコピーを返す
備考:配列は低レイヤな扱いになってしまうので、
java.util.Collections
を使うようにすると良さそう。
2. 項目 16 public のクラスでは、public のフィールドではなく、アクセッサーメソッドを使う
2.1. 結論
- そのクラスのパッケージ外からアクセス可能であれば、アクセッサーメソッドを提供するべき
- クラスがパッケージプライベート/private のネストしたクラスの場合、そのフィールドは公開しても良い
2.2. そのクラスのパッケージ外からアクセス可能であれば、アクセッサーメソッドを提供するべき
メソッドにはsynchronized
が付けれるが、フィールド値にはsynchronized
を付けれないのでgetter
/setter
を利用するようにしましょう。
|
|
2.3. クラスがパッケージプライベート/private のネストしたクラスの場合、そのフィールドは公開しても良い
外部からアクセスされる心配がなく、コードの可読性が上がるから。
Intellij
やEclipse
の機能で自動的に検出されるから、意識しなくても大丈夫そう。
3. 項目 17 可変性を最小限にする
3.1. 不変クラスとは
インスタンスに保持される全ての情報は、生成時に提供され、その後は変更できないクラスのこと。
例として、java.lang.String
、基本データ型のラッパークラス(java.lang.Integer
等)、java.math.BigInteger
、java.math.BigDecimal
などが挙げられます。
備考:
java.lang.Integer
のオブジェクトで、以下のようなコードを実行すると、毎回新しいインスタンスが生成される。
|
|
3.2. 不変クラスの規則
setter
などオブジェクトの状態を変更するためのメソッドは提供しない。- クラス拡張できないように、
final class
としましょう。 - 変更不可能とするために、すべてのフィールドに
private final
を付けましょう。
3.3. 不変クラスのメリット
- スレッドセーフであること
- 防御的コピーの必要なし、clone メソッドやコピーコンストラクタ作成の必要もなし
- 内部の状態も共有可能
3.4. 不変クラスのデメリット
- 一部だけ変更したいだけなので、全てを再生成しなければならないのがデメリット、、、しかし、コンパニオンクラスを使えば解決できる!(例:
java.lang.StringBuilder
)
備考:コンパニオンクラスの
java.lang.StringBuilder
について調べてみた。AbstractStringBuilder#append
を見てみると、内部でchar[] value
のように持っておき、java.lang.String
を何度も生成しないように工夫していた。
|
|
4. 項目 18 継承よりコンポジションを選ぶ
4.1. 結論
継承は、実装全部を継承することを理解して使う。
機能を流用したいだけならば、コンポジションを使うことが適切なことが多い。
is-a 関係にあれば使える局面があるが、使うべき局面は多くない。
本当に多重階層にすることが妥当かを検討すべし。
4.2. 継承/コンポジションとはなにか
- 継承
- 上位の性質をすべて下位に受け継ぐ
- 以下のときは使える
- スーパークラスもサブクラスも特定のパッケージ配下にある場合
- 拡張のために設計・文書化されているクラスを拡張する場合
- インタフェース継承の場合
- コンポジション
- 英語辞書 的には、混成/合成 という意味。
- A クラスのなかに、B クラスをもっている状態
- is-a ではなく has-a になっている状態
4.3. 継承のデメリット
継承をした際に、サブクラスの実装によってクラスの整合性が失われやすい。
|
|
上記クラスは一見、正しく動くように見えるが、現実には正しく機能しない。
|
|
なぜなら、HashSet の addAll は、内部的に add メソッドを利用して実装されているため。
4.4. コンポジションの例
コンポジションは、 処理を他のクラスにお願いする
と思っておけばよい。
以下が例。
|
|
使うときの例
|
|
Spring で利用されているコンポジションの例
|
|
様々なItemWriterを合成(Composite)して 、同じように扱って書き込み処理している。
4.5. コンポジションと委譲
コンポジションで実現することは転送(forward)と訳される事が多い。
その他一般的な用語では、委譲(delegate)という言葉がある。
この 2 つの違いは曖昧。
一節には、
|
|
といわれるが、これも誤りらしい。
たいていの開発者は、コンポジションを委譲と同義で使うと理解すれば良さそう。
5. 項目 19 継承のために設計および文書化する、でなければ継承を禁止する
5.1. 結論
一言でいってしまえば、protected なクラス/メソッドは、拡張される可能性があるため、 継承した場合にどのように使うのか、ルール/制約等を全部記述すべきだということ。
5.2. 理由
第 18 節の通り、かんたんに不整合がうまれるため。 これを防ぐには手厚いドキュメントが必要。
とはいえ、
本来、良い API ドキュメントは何するのかを記述し、どのように行うかは記述するべきではない。 継承されるクラスには、実装の詳細を記述する必要がある。理由は、継承によってカプセル化が破壊されやすいため。
という一文の通り、良い API ドキュメントでは、手厚いドキュメントはあまり好ましいことではない。 付け加えるならば、本来 API は何も考えずに使える(脳死できる)ことが望ましく、 最新の注意を払う時点でバグを生む。
5.3. 継承できることを担保する
継承されるクラスを作成する際、
実際に、そのクラスを継承したクラスを用いたテストを十分にする必要がある。
拡張前提の AP フレームワークの場合、これを十分行う必要がある。
5.4. 継承させるべきでないもの
Cloneable
とSerializable
- この2つのどちらかのインタフェースを実装すると、クラスを継承するユーザにかなりの実装の負担をかける。
Cloneable
についてはこちらを参照してください。Serializable
は、すべてのフィールドを serialize 可能 or 除外することを考えなくてはならない。
- この2つのどちらかのインタフェースを実装すると、クラスを継承するユーザにかなりの実装の負担をかける。
5.5. 継承させる / させない ことを考える
- 継承させないなら、コンストラクタを
private
かパッケージプライベート
にする or クラスをfinal
にする - 継承させた上で、あるメソッドは参照させるのみならば
final
にする- どこまでやるか、は、使う人がだれか、を念頭に置くべし
- 身内で、かつ、コントロールしきれる範囲のものならば、そこまで厳密でなくて OK
- 世界中に公開される OSS ならば、厳密すぎるっていうことはない
- どこまでやるか、は、使う人がだれか、を念頭に置くべし
6. 項目 20 抽象クラスよりインタフェースを選ぶ
以下の点で、インタフェースの方が抽象クラスよりも優れているので、インタフェースを選びましょう。
- 既存クラスに新たなインタフェースを実装しやすいこと
- インタフェースは、
Mixin
を定義するには理想的であること- 多重継承可であるから。
- 階層を持たない型フレームワークを構築できること
- 抽象クラスと組み合わせること(骨格実装クラス)で、よりユーザーにとって便利になること
- ユーザがメソッドを実装しなくても良い。
ただし、public
のインタフェースは「リリース後の変更が不可である」ということを意識しておきましょう。(設計は注意深く、実装クラスのテストもしっかりと行いましょう。)
6.1. 抽象クラスとインタフェース
抽象クラスとインタフェースの違いは以下の 2 点。
- 実装を
含めれる
か否
か - 継承が
1:1
か1:多
か
6.1.1. 抽象クラス
|
|
6.1.2. インタフェース
|
|
備考:java8 から Interface にデフォルトメソッドを定義できるようになりましたね。
6.2. インタフェースの Good Point
6.2.1. 既存クラスに新たなインタフェースを実装しやすいこと
インタフェースでは単に実装するだけで良いが、抽象クラスでは継承階層を意識しなければいけない。(抽象クラスが、複数の既存クラスで継承されていたら悩まされることに、、、)
-
インタフェースの場合
- 単に拡張したい既存クラスのクラス宣言に、
implements
節を追加して、メソッドを実装するだけで良い。
- 単に拡張したい既存クラスのクラス宣言に、
-
抽象クラスの場合
- 継承階層のどの位置に差し込むかについて考えた上で、
extends
節を追加する必要がある。
- 継承階層のどの位置に差し込むかについて考えた上で、
6.2.2. インタフェースは、Mixin
を定義するには理想的であること
インタフェースは多重継承可能であるため、Mixin
ができる。
Mixin
とは「クラスが「本来の型」に加えて、なんらかの任意の振る舞いを提供していることを宣言するための型のこと」である。
- 例:Comparable
- 「他の相互比較可能なオブジェクトに対しての順序付けされていること」を本来の機能に加えて宣言できるから。
6.2.3. 階層を持たない型フレームワークを構築できること
フレームワークを構築する前提で、N
個の属性をサポートすることを考える。
抽象クラスの場合の場合、属性ごとに階層を考慮する必要があるが、インタフェースではその必要がない。
- インタフェースの場合
- フレームワーク構築者は、
N
個のインタフェースを作成すれば良い。 - ユーザは、利用したい属性を、
implements
節に足すだけで良い。
- フレームワーク構築者は、
|
|
- 抽象クラスの場合
- フレームワーク構築者は、
N
個の属性を利用するか否かの2^N
個のクラスを作成する必要がある。(組み合わせ爆発と呼ばれる事柄で、対処しづらくなっていく。) - ユーザは、利用した属性を
2^N
個のクラスの中から選択し、そのクラスを継承し利用する。
- フレームワーク構築者は、
|
|
6.2.4. 抽象クラスと組み合わせること(骨格実装クラス)で、よりユーザーにとって便利になること
骨格実装クラスにより、プログラマがインタフェースを独自実装する際の手助けになる。下記、setValue(V value)
メソッドのコメントはありがたいですね。
下記、 AbstractMapEntry.java の良い点は、単なる抽象クラスではなくて、インタフェースを実装しているから、型定義に関しての制約が課されることがない。
AbstractMapEntry.java
|
|
6.3. 余談(抽象クラスの利点)
**「抽象クラス : 実装クラス = 1 : 多」**となるのであれば、インタフェースよりも抽象クラスを使いましょう。理由は、抽象クラスにメソッドを追加すれば、全ての実装クラスに反映されて容易だから。(インタフェースの場合は、反映先のクラス数が多く、大変になる。)
極端な例ですが、、今から
List<E>
インタフェースに新たな public メソッドを追加すると、影響範囲は大変なことになりますよね。笑
7. 項目 21 将来のためにインタフェースを選ぶ
Java8 ではデフォルトメソッド構文が追加されたが、以下のことには気をつけておきましょう。
- 既存の実装クラスに新規デフォルトメソッドを反映させて、エラーや警告なしでコンパイルできるかもしれないが、実行時に失敗することがある。
- デフォルトメソッドを使うことになっても、インタフェースの設計が重要なことには変わりない。
- リリース後の修正は困難なので、3 つの異なる実装に対して十分テストを行うことが大切である。
例として、Collection に removeIf がデフォルトメソッドとして追加された。
しかし、Collection
をimplements
している Apache Commons ライブラリの SynchronizedCollection でこのremoveIf()
を呼び出した際には、同期されないメソッドとして呼び出され、SynchronizedCollection
使用における約束事が破られてしまう。
Java ライブラリではこれと同様のことを防ぐため、Collections.synchronizedCollection において removeIf() をオーバーライドして対処している。
8. 項目 22 型を定義するためだけにインタフェースを使う
インタフェースの使用方法
- 型を定義するためだけに使用する。
- 定数を提供するためには使用すべきではない。
8.1. 型を定義するためだけに使用する。
インタフェースは、実装したクラスのインスタンスを参照するのに使用できる型(type)として機能する。(型(_type_) = 「何ができるのかを述べている。」
)
「何ができるのかを述べている。
」以外の目的のために定義するのは不適切である。
8.2. 定数を提供するためには使用すべきではない。
8.2.1. 定数インタフェース
定数インタフェース(例:java.io.ObjectStreamConstants.java
)は、「実装クラスに、この定数に関する責務を負わせることになる」ため使うべきではない。
定数を利用したい場合は、Enum 型や定数ユーティリティクラス(例:org.springframework.core.Constants.java)を利用しましょう。
9. 項目 23 タグ付クラスよりクラス階層を選ぶ
9.1. 結論
- タグ付きクラスが適切な場合はほとんどない
- タグ付きクラスを作りたくなったら、またはタグ付きクラスに出くわしたら、クラス階層で置き換えられないかを検討する
9.2. タグ付きクラスとは
- 次のような、インスタンスが複数の特性を持っていて、その特性を示すためにタグフィールドを持つクラス
|
|
9.3. タグ付きクラスの欠点
- 複数の実装が単一クラスに詰め込まれているので、可読性が低い
- 他の特性に属する関係のないフィールドを含んでいるので、余分にメモリを使う
- 誤ったフィールドを初期化しても、コンパイラは検知せず、実行時に失敗する
- 例として、次のように、誤ったタグフィールドで初期化しても、実行時に失敗するまでわからない
|
|
- 新しい特性を追加するには、ソースコードを修正しなければならない
9.4. 解決策
- クラス階層を用いる
|
|
9.5. クラス階層の良い点
- タグ付きクラスの欠点がすべて解決できている
- 複数の実装が単一クラスに詰め込まれていない
- 他の特性に属する関係のないフィールドを含んでいない
- 誤ったフィールドを初期化することはない
- 型間の自然な階層関係を反映できる
- 例えば、正方形はクラス階層を利用して以下のように作成できる
1 2 3 4 5
class Square extends Rectangle { Square(double side) { super(side, side); } }
- 例えば、正方形はクラス階層を利用して以下のように作成できる
10. 項目 24 非 static のメンバークラスより static のメンバークラスを選ぶ
10.1. 結論
- ネストしたクラスはエンクロージングクラスに対して仕える目的のためだけに存在すべき
- (エンクロージングクラスは内部クラスにとっての外部クラス)
- ネストしたクラスが、他の何らかの状況で有用なのであれば、トップレベルのクラスにするべき
- ネストしたクラスは 4 種類あり、それぞれ用途が異なる
- static のメンバークラスはクラスに関連したクラス
- 非 static のメンバークラスはインスタンスに関連したクラス
- 無名クラスはその場限りのクラス
- ローカルクラスもローカル変数のクラス版
- メンバークラスやローカルクラスは、外部クラスを離れては意味のないクラスを内部に隠蔽することができるのでカプセル化の点でメリットがある。(参考:java の内部クラスおさらい)
10.2. ネストしたクラスの種類
- static のメンバークラス
- 非 static のメンバークラス
- 無名クラス
- ローカルクラス
|
|
10.3. static のメンバークラス
10.3.1. 特徴
- 最も単純なネストしたクラス
- 通常の別クラスとして宣言し場合と近い感覚で利用できる
- 通常のクラスとの違いはエンクロージングクラスの static のメンバーにアクセスできること
- メンバーなので private,protected が使える
10.3.2. 使用例
- その外部クラスを使用と一緒に使用すると有用な public のヘルパークラス
- 例えば、その外部クラスだけに関連している enum クラス
- (内部 enum は static キーワードをつけなくても static として定義される。参考:内部 enum は static 扱い)
- 何かに属していることを明示したり、ジェネリクスを合わせたりといったメリット
- Map.Entry (public クラスの public なインターフェース)
- 例えば、その外部クラスだけに関連している enum クラス
10.4. 非 static のメンバークラス
10.4.1. 特徴
- エンクロージングインスタンスと関連付けられている
- エンクロージングインスタンスなしで、非 static のメンバークラスのインスタンスは生成できない
- エンクロージングクラスのメンバーすべてにアクセスできる
- メンバーなので private,protected が使える
10.4.2. 使用例
- エンクロージングクラスのインスタンスを関係のないクラスのインスタンスとしてみなすことを可能にする Adapter を定義するときに使う - Adapter パターンは既存クラスを修正することなく、 異なるインタフェースを持たせることができるデザインパターン
1 2 3 4 5 6 7 8 9 10 11 12 13 14
public class MySet<E> extends AbstractSet<E>{ // 省略 public Iterator<E> iterator(){ return new MyIterator(); } private class MyIterator implements Iterator<E>{ ... } } ``` > static のメンバークラスと同様に何かに属していることを明示したり、ジェネリクスを合わせたりといったメリットがある
10.4.3. 主張
- エンクロージングインスタンスへのアクセスが必要なければ、static のメンバークラスを利用するべき
- static 修飾子をつけないと、個々のインスタンスはエンクロージングインスタンスへの参照を持ち、エンクロージングインスタンスが適切に GC されない可能性があるから(項目 22:廃れたオブジェクト参照を取り除く)
- 以下の例のような状況がある。
1 2 3 4
class Outer{ class Inner{ } }
1 2 3
var o = new Outer(); var i = new o.Inner(); o = null; //i から o への参照は残る
- static 修飾子をつけないと、個々のインスタンスはエンクロージングインスタンスへの参照を持ち、エンクロージングインスタンスが適切に GC されない可能性があるから(項目 22:廃れたオブジェクト参照を取り除く)
10.5. 無名クラス
10.5.1. 特徴
- 他のネストしたクラスとは全く異なる
- クラス名が無い
- エンクロージングクラスのメンバーではない
- 式が許されている場所であれば、コードのどの場所でも宣言できる
- 宣言と同時にインスタンス化される
- 非 static の文脈内で書かれたならば、エンクロージングインスタンスを持つ
- static の文脈内で書かれても static のメンバーを持つことができない
- instanceof 検査やクラスの名前を指定する必要がある処理はできない
- 外側で定義されているローカル変数を使う場合は、そのローカル変数に暗黙に final が付いているものと見なされる(Java クラス使用メモ(Hishidama’s Java Class use Memo))
10.5.2. 使用例
-
その場で関数オブジェクトを生成
- Comparator
1 2 3 4 5
Collections.sort(list, new Comparator<Hoge>() { public int compare(Hoge o1, Hoge o2) { return o1.id.compareTo(o2.id); } });
- Comparator
-
プロセスオブジェクトを生成
- Thread
- Runnable
- TimerTask
Java で Thread を実装して、無名クラス、ラムダを使ってみる - Qiita
1 2 3 4 5 6 7 8 9
public static void main(String[] args) { new Thread(new Runnable() { public void run(){ for (int i=0; i < 500; i++){ System.out.print('*'); } } }).start(); }
-
static ファクトリーメソッド内での使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
static List<Integer> intArrayAsList(final int[] a) { if (a == null) { throw new NullPointerException(); } return new AbstractList<Integer>(){ @Override public int size() { return a.length; } 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; } }; }
-
ラムダ式
1 2 3
hogeList.stream() .forEach((i) -> { System.out.print(i + " "); } );
- 無名クラスの形に戻すと、以下。
1 2 3 4 5 6 7
hogeList.stream() .forEach(new Consumer<Integer>() { public void accept(Integer i) { System.out.print(i + " "); } });
- ラムダ式をコンパイルしたものを逆コンパイルしたのを見ると、無名クラスの形になる。
10.6. ローカル内部クラス
10.6.1. 特徴
- private,protected が使えない
- 外部クラスのメンバーにはアクセス可能
- 外側で定義されているローカル変数を使う場合は、そのローカル変数に暗黙に final が付いているものと見なされる
10.6.2. 主張
- あまり使われない
- 短くあるべき
- ローカル変数のようなもの
11. 項目 25 ソースファイルを単一のトップレベルのクラスに限定する
11.1. 結論
- 単一のソースファイルに複数のトップレベルのクラスを作るべきでない
11.2. 理由
- コンパイルの順序によって結果が変わってしまうことがあるから
- 例えば、下記のような Main クラス、
Utensil.java
、Dessert.java
を考えるときに、javac Main.java
または、javac Main.java Utensil.java
でコンパイルして実行すると、pancake
が表示されるjavac Main.java Dessert.java
でコンパイルして実行すると、potpie
が表示される- main メソッド
System.out.println(Utensil.NAME + Dessert.NAME)
をSystem.out.println(Dessert.NAME + Utensil.NAME)
に変更してjavac Main.java
でコンパイルして実行すると、piepot
が表示される(Dessert.java の方)
- 例えば、下記のような Main クラス、
Main.java
|
|
Utensil.java
|
|
Dessert.java
|
|
11.3. 補足:javac の依存関係の解消の仕組み
|
|
11.4. 解決法
-
1つソースファイルには 1 つだけトップレベルのクラスを作る
-
static なメンバクラスとして定義する
1 2 3 4 5 6 7 8 9 10 11 12 13
public class Main { public static void main(String[] args) { System.out.println(Utensil.NAME + Dessert.NAME); } private static class Utensil { static final String NAME = "pan"; } private static class Dessert { static final String NAME = "cake"; } }
11.5. 意見
- FQCN が完全に同じクラスが2つあった時、先に読み込んだ方のクラスを使う