Java中級者以上の必須本である、Effective Java 第3版に Kindle版が出たので、まとめる。
前:Effective Java 第3版 第3章全てのオブジェクトに共通のメソッド
次:Effective Java 第3版 第5章ジェネリックス
項目15 クラスとメンバーへのアクセス可能性を最小限にする
- 各クラスやメンバーをできる限りアクセスできないようにするべき。
トップレベルのクラスやインタフェース
- publicとパッケージプライベートがあり、できる限りパッケージプライベートにするべき。パッケージの公開APIではなくなるため。
- トップレベルのパッケージプライベートのクラスやインタフェースが、一つのクラスだけが使っているならば、そのクラス内でprivateのネストしたクラスにすることを検討する。
メンバー(フィールド、メソッド、ネストしたクラス、ネストしたインタフェース)
- 最初は全てprivateにして、同じパッケージの他のクラスへアクセスする必要があるときだけ、パッケージプライベートにするべき。
- publicクラスのメンバーに関して、パッケージプライベートからprotectedに変更された場合、アクセス可能性が大幅に増加する。
- protectedのメンバーの必要性は、比較的まれであるべき。
- スーパクラスのメソッドをオーバーライドするとき、スーパークラスでメソッドが持っているアクセスレベルを、サブクラスで狭めるようなアクセスレベルは許されていない(Javaの仕様)
- テストを容易にするために、クラス、インタフェース、メンバへのアクセス制限を緩めることは、ある程度まで許される。privateをパッッケージプライベートまで。それ以上は受け入れられない。
- インスタンスフィールドは、publicにすべきではない。finalではない、可変オブジェクトの参照だった時、そのフィールドに保存できる値を制限することを放棄するから。結果として、publicの可変フィールドを持つクラスは、一般的にスレッドセーフではない。
- 定数を公開するには、public static finalを使う。慣習として、大文字でアンダースコアで区切られた名前を用いる。基本データ型か、不変オブジェクトへの参照のどちらかを持つことが不可欠。値が変更されてしまうため。長さがゼロではない配列は常に可変であることに注意する。
// 潜在的なセキュリティホール(配列の中身が変更できてしまう)
public static final Thing[] VALUES = { ... };
// 解決方法1. publicの不変リストを返す
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
// 解決方法2. publicメソッドで、配列のコピーを返す
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
return PRIVATE_VALUES.clone();
}
項目16 publicのクラスでは、publicのフィールドではなく、アクセッサーメソッドを使う
- publicのクラスは、可変フィールドを公開するべきでない。privateのフィールドとpublicのアクセサーメソッド(ゲッター)を持ち、可変クラスに対してはミューテーター(セッター)を持つようにするべき。
- パッケージプライベートかprivateのネストしたクラスの場合、そのデータフィールドを直接公開することは本質的には問題がない。
項目17 可変性を最小限にする
- 不変オブジェクトは単純。本質的にスレッドセーフ。
- 不変クラスに対して、cloneメソッドやコピーコンストラクタを提供する必要なく、提供するべきはない。
- クラスを不変にするには、次の五つの原則に従う。
- オブジェクトの状態を変更するためのメソッドを提供しない。
- クラスを拡張できないようにする。
- 全てのフォールドをfinalにする。
- 全てのフォールドをprivateにする。
- 可変コンポーネントに対する独占的アクセスを保証する。
- 不変オブジェクトの実質的欠点は、個々の異なる値に対して別々のオブジェクトを必要とすること。
- 可変にすべき正当な理由がない限り、クラスは不変にすべき。
- クラスを不変にできないなら、その可変性をできるだけ制限すべき。
項目18 継承よりコンポジションを選ぶ
- 継承は不適切に使われると、もろいソフトウェアを作り出す。継承はカプセル化を破る。サブクラスが適切に機能するかは、スーパークラスの実装に依存する。
- 既存のクラスを拡張する代わりに、新たなクラスに既存のクラスのインスタンスを参照するprivateのフィールドを持たせる。この設計はコンポジションと呼ばれる。
- 新たなクラスの各インスタンスメソッドは、保持している既存のクラスのインスタンスに対応するメソッドを呼び出してその結果を返す。これは転送と呼ばれ、新たなクラスのメソッドは転送メソッドと呼ばれる。
- コンポジションと転送の組み合わせが、大雑把に委譲と呼ばれることがある。技術的には、ラッパーオブジェクトがラップしているオブジェクトへ自分自身を渡さない限り、委譲ではない。
項目19 継承のために設計および文章化する、でなければ継承を禁止する
- 継承のためにクラスを設計することは、そのクラスにかなりの制限を課す。クラスの自己利用を全て文章化しなければらないし、一旦文章化したらクラスが存在する限りそれを守らなければならない。
-
@implSpec
のJavadocタグを使って継承のための文章化を行う。 - 最善の解決法は、安全にサブクラス化されるための設計も文章化もされていないクラスのサブクラス禁止をする。サブクラスを禁止する方法は二通り。
- クラスをfinal宣言する。
- クラスのコンストラクタをprivateかパッケージプライベートにして、コンストラクタの代替としてpublicのstaticファクトリメソッドを追加する。
項目20 抽象クラスよりもインタフェースを選ぶ
- インタフェースのあるメソッドの明らかな実装が存在する場合、デフォルトメソッド形式で実装補助の提供を検討する。equals、hashCodeといったObjectのメソッドに対するデフォルトメソッドの提供は許されていない。
- インタフェースはインスタンスフィールドやpublicではないstaticのメンバー(private staticのメソッド以外)を含むことは許されていない。
- インタフェースに付随する抽象骨格実装クラスを提供することで、インタフェースと抽象クラスの長所を組み合わせられる。骨格実装はインタフェースの基本メソッドに基づいて、インタフェースの残っている基本ではないメソッドを実装する。骨格実装を拡張することで、インタフェースを実装することのほとんどが行われる。(Template Method パターン)
- 慣習として、骨格実装クラスはAbstract Interface と命名される。
- 例えばコレクションフレームワークは、主要なコレクションインタフェースごとに付随する骨格実装を提供されていて、AbstractCollection、AbstractSet、AbstractList、AbstractMap。
- 骨格実装の優れた点は、抽象クラスが型定義の役割を果たす際の深刻な制約を課すことなく、抽象クラスである実装補助を提供することである。
- インタフェースを実装しているクラスは、骨格実装を拡張したprivateの内部クラスのインスタンスを保持しておいて、インタフェースの呼び出しをそのインスタンスへ転送できる。この技法を擬似多重継承と呼ばれる。
項目21 将来のためにインタフェースを設計する
- Java8より前は、インタフェースに新たなメソッドを追加した時、既存の実装はまだそのメソッドを持っておらず、コンパイルエラーになる。Java8ではデフォルトメソッド構文が追加されたが、既存のインタフェースへのメソッド追加はリスクを伴う。
- デフォルトメソッドは、インタフェースの実装をエラーや警告なしでコンパイルできるかもしれないが、実行時に失敗することがある。
- デフォルトメソッドはJavaプラットフォームの一部であっても、注意深いインタフェースの設計が重要なことには変わりない。メソッドの追加には大きなリスクを伴う。
- 新たなインタフェースをリリースする前に、そのインタフェースをテストすることが重要。最低、3つの異なる実装を行いテストするべき。インタフェースの欠陥をリリース後に修正を行うことは困難を伴う。
項目22 型を定義するためだけにインタフェースを使う
- インタフェースは、クラスを実装した場合、そのインスタンスを参照するのに使える型として機能する。
- 定数を定義するためだけのインタフェースである、定数インタフェースを使うべきではない。
- 定数は、enum型か、インスタンス不可能なユーティリティクラスで提供すべきである。
項目23 タグ付きクラスよりもクラス階層を選ぶ
- クラスが2つ以上の特性を持っていて、それをenumで切り替えるようなクラスをタグ付きクラスと言い、多くの欠点を持っている。読みにくく、メモリ量の増加、冗長、謝りやすく非効率。
- 複数の特性のオブジェクトを表現できる単一のデータ型を定義するには、サブタイプ化をする。
- 個々の操作に対する抽象メソッドを含んだ抽象クラスを定義する。共通の振る舞いやデータがあれば抽象クラスに入れる。
- 個々の特性に対して、ルートクラスの具現サブクラスを定義する。
項目24 非staticのメンバークラスよりもstaticのメンバークラスを選ぶ
- 4種類のネストしたクラス
- staticのメンバークラス(内部クラス)
- 非staticのメンバークラス
- 無名クラス
- ローカルクラス
- staticと非staticのメンバークラスの構文的な唯一の相違点は、staticのメンバークラスはその宣言にstatic修飾子があること。これらの2種類のネストしたクラスは異なる。
- 非staticのメンバークラスの個々のインスタンスは、それを含むクラスのエンクロージングインスタンスと暗黙に関連付けされている。非staticのメンバークラスのインスタンスメソッド内では、エンクロージングインスタンスに対してメソッドを呼び出すことができ、あるいはエンクロージングインスタンスへの参照を修飾されたthis構文を使って得られる。
- (加筆予定)
staticなメンバークラス
class Outer {
private int outerField;
private static int a;
// staticなメンバークラス
static class Inner {
void innerMethod() {
// staticな外部クラスメンバのみ利用可
a = 10;
}
}
// 外部クラスの中にあるメソッド
void outerMethod() {
Inner ic = new Inner();
}
}
class Main {
public static void main(String[] args) {
// Outer.Innerで利用
Outer.Inner ic = new Outer.Inner();
ic.innerMethod();
}
}
非staticなメンバークラス
class Outer {
private static int outerField;
private int b;
// 非staticなメンバークラス
class Inner {
void innerMethod() {
outerField = 10;
b = 5;
}
}
// 外部クラスの中にあるメソッド
void outerMethod() {
Inner ic = new Inner();
}
}
class Main {
public static void main(String[] args) {
Outer o = new Outer();
// o.newで利用
Outer.Inner in = o.new Inner();
}
}
項目25 ソースファイルを単一トップレベルのクラスに限定する
- 単一のソースファイルに複数のトップレベルのクラスを入れない。理由は、コンパイラが正しくクラスを認識しない可能性があるから。