もっとちゃんとJavaのこと理解したいなと思って、Effective Javaを読んだ。
オブジェクトの生成と消滅
コンストラクタの代わりにstaticファクトリメソッドを検討する
-
staticファクトリメソッドのメリット
- コンストラクタと異なって、名前を持つから返されるオブジェクトが何者かわかりやすい
- コンストラクタと異なって、呼び出しごとに新たなオブジェクトを生成する必要がない
- コンストラクタと異なって、メソッドの戻り値型の任意のサブタイプのオブジェクトを返せる
- 返されるオブジェクトのクラスは、入力パラメータの値に応じて呼び出しごとに変えられる
- 返されるオブジェクトのクラスは、そのstaticファクトリメソッドを含むクラスが書かれた時点で存在する必要さえない
-
staticファクトリメソッドのデメリット
- publicあるいはprotectedのコンストラクタを持たないクラスのサブクラスを作れない制約がある
- プログラマがstaticファクトリメソッドを見つけるのが難しい
-
共通する命名規約を遵守することで、軽減できる
- from:単一パラメータを受け取り、当の肩を持つ対応するインスタンスを返す型変換メソッド
- of:複数のパラメータを受け取り、それらを含んだ当の型のインスタンスを返す集約メソッド
- valueOf:fromやofの代わりとなる、冗長な名前のメソッド
- instance あるいは getInstance:パラメータがあればそのパラメータで表されているインスタンスを返すが、必ずしも同じ値を持つとは限らない
- create あるいは newInstance:呼び出しごとにメソッドは新たなインスタンスを返す
- getType:ファクトリメソッドが対象のクラスとは異なるクラスにある場合に使われる。Typeはファクトリメソッドから返されるオブジェクトの型を示している。(getInstanceと類似)
- newType:ファクトリメソッドが対象のクラスとは異なるクラスにある場合に使われる。Typeはファクトリメソッドから返されるオブジェクトの型を示している。(newInstanceと類似)
- type:getTypeやnewTypeの代わりとなる、簡潔な名前のメソッド。
多くのコンストラクタパラメータに直面したときはビルダーを検討する
テレスコーピング・コンストラクタ・パターンは、多くのパラメータがある場合、クライアントのコードを書くのが困難になり、可読性が落ちる。JavaBeansパターン(setter)は、生成過程の途中で不整合な状態にあるかもしれない。クラスを不変にする可能性を排除してしまう。
テレスコーピング・コンストラクタ・パターンの安全性とJavaBeansパターンの可読性を組み合わせたのがBuilderパターン。Builderパターンは、コードを書くのも読むのも容易になる。
Builderパターンの欠点は、「最初にビルダーを生成しないといけない」、「記述が長くなってしまう」。将来的にパラメータが増えそうなときは、最初からビルダーで始めるのがたいていよい。
ビルダーによるクライアントのコードは、テレスコーピング・コンストラクタ・パターンよりは読みやすく、JavaBeansバターンよりは安全。
privateコンストラクタかenum型でシングルトン特性を強制する
インターフェースを実装していない限り、シングルトンをモック実装で置き換えることが不可能なため、クラスをシングルトンにすると、クライアントのテストを困難にする。
-
シングルトンを実装するための方法
- public finalのフィールド
- staticファクトリメソッド
- 単一要素を持つenumを宣言
シングルトンを保証し続けるには、すべてのインスタンスフィールドをtransientと宣言して、readResolveメソッドを提供しなければならない。そうしなければ、シリアライズされたインスタンスをデシリアライズするごとに、新たなインスタンスが生成されてしまう。
資源を直接結び付けるよりも依存性注入を選ぶ
多くのクラスが1つ以上の下層の資源に依存している。
クラスの複数のインスタンスをサポートして、それぞれのインスタンスでクライアントが望む資源を使えることが必要で、新しいインスタンスを生成するときにコンストラクタに資源を渡すことが単純な解決策になる。
依存性注入は柔軟性とテスト可能性を大幅に向上するが、普通に数千の依存を含むような大きなプロジェクトを撮り散らかす可能性があるが、依存性注入フレームワークを使えば取り除ける。
不必要なオブジェクトの生成を避ける
機能的に同じオブジェクトが必要な度に新たに生成するよりは、一つのオブジェクトを再利用する方が適切。オブジェクトが不変であれば、常に再利用できる。
使われなくなったオブジェクト参照を取り除く
ガベージコレクションを持つ言語は、オブジェクトを使い終えたときにそれが自動的に回収されることで、プログラマとしての仕事は楽になる。
スタックが大きくなってその後小さくなると、スタックから取り出されたオブジェクトはガベージコレクトされない。スタックはそれらのオブジェクトに対する使われなくなった参照を保持しているため。
クラスが独自のメモリを管理しているときは、プログラマはメモリリークに対して注意を払うべき。要素が解放されるときには、その要素に含まれたすべてのオブジェクト参照にnullを設定すべき。
ファイナライズとクリーナーを避ける
ファイナライザは予想不可能で、たいていは危険で、一般的には必要ない。
-
ファイナライザとクリーナーの欠点
- 即座に実行される保証がない。オブジェクトが到達不可能となってから実行されるまでの時間は、任意の長さ。
セーフティネットとして、あるいは重要でないネイティブピアの資源の解放のためを除いて、クリーナー(またはファイナライザ)は使わない方がよい。
try-finallyよりもtry-with-resourcesを選ぶ
Javaライブラリは、closeメソッドを呼び出して手作業でクローズしなければならない多くの資源を含んでいる。例として、InputStream、OutputStream、java.sql.Connectionが含まれる。
クローズしなければならない資源を扱うときは、いつでもtry-with-resourcesを使うのがよい。その結果、コードは短くて明瞭で、生成される例外は有用なものになる。
すべてのオブジェクトに共通のメソッド
Objectは具象クラスであるにもかかわらず、主に拡張されるために設計されている。 そのfinalでないすべてのメソッド(equals, hashCode, toString, clone, finalize)は、オーバーライドされるように設計されているおり、明示的な一般契約を持っている。
equalsをオーバーライドするときは一般契約を使う
equalsメソッドのオーバーライドによる問題を防ぐ1番簡単な方法は、オーバーライドしないこと。その場合、個々のインスタンスは自分自身とだけ等しくなる。
次の条件のどれかが当てはまるなら、オーバライドしないのが正しいやり方。
- クラスの個々のインスタンスが本質的に一意である
- クラスが「論理的等価性」の検査を提供する必要がない
- スーパークラスがすでにequalsをオーバーライドしており、スーパークラスの振る舞いがこのクラスに対して適切である
- クラスがprivateあるいはパッケージプライベートであり、そのequalsメソッドが呼び出されないことが確かである
オーバーライドするのが適切なときは、クラスが単なるオブジェクトの同一性とは異なる論理等価性という概念を持っていて、かつスーパークラスがequalsメソッドをオーバーライドしていないとき。一般には、値クラスのとき。
equalsメソッドのオーバーライド時に厳守すべき5要件
-
反射性
- オブジェクトがそれ自身と等しくなければならない
-
対称性
- いかなる二つのオブジェクトでも、それらが等しいかどうかについて合意しなければならない
-
推移性
- 一つ目のオブジェクトが二つ目のオブジェクト等しく、かつ、二つ目のオブジェクトが三つ目のオブジェクトと等しければ、最初のオブジェクトと三つ目のオブジェクトは等しくなければならない
-
整合性
- 二つのオブジェクトが等しければ、どちらか片方あるいは両方が変更されない限り、いつまでも常に等しくあり続けなければならない。
-
非null性
- すべてのオブジェクトはnullと等しくあってはならない
高品質のequalsメソッドを作成するレシピ
- 引数が自分自身のオブジェクトへの参照であるか検査するために==演算子を使う
- 引数が正しい型であるか検査するためにinstanceof演算子を使う
- 引数を正しい型にキャストする
- クラスの「意味のある」フィールドのそれぞれについて、引数のオブジェクトのフィールドが、このオブジェクトの対応するフィールドと一致するか検査する
equalsをオーバーライドするときは、常にhashCodeをオーバーライドする
equalsをオーバーライドしているすべてのクラスで、hashCodeをオーバーライドしなければならない。そうしないクラスは、Object.hashCodeの一般契約を破ることになり、HashMap、HashSetなどのコレクションで用いられると適切に機能しない。
toStringを常にオーバーライドする
Object#toStringは、クラス名、「@」、ハッシュコードの符号なし16進数表現から構成されており、一般的にクラスのユーザが見たい内容ではない。実用的な場合、toStringメソッドはオブジェクトに含まれる興味があるすべての情報を含むべき。
cloneを注意してオーバーライドする
メソッドを含んでいないCloneableはObjectのprotectedのcloneメソッドの実装の振る舞いを決定する。Cloneableを実装しているクラスは、適切に機能するpublicのcloneメソッドを提供することが期待されている。
Comparableの実装を検討する
compareToメソッドはObjectでは宣言されていない。正確に言えば、compareToメソッドはComparableインターフェースの唯一のメソッド。Comparableを実装することで、インスタンスが自然な順序を持っていることをクラスは示している。
アルファベット順、数値順、年代順などの明らかに自然な順序を持つ値クラスを書くなら、Comparableインターフェースを実装すべき。compareToメソッドの実装内でフィールドの値を比較するときは、<演算子と>演算子を使わないほうがよい。代わりに、ボクシングされた基本データクラスのstaticのcompareメソッドか、Comparatorインターフェースのコンパレータ構築メソッドを利用する。
クラスとインターフェース
クラスとメンバーへのアクセス可能性を最小限にする
うまく設計されたコンポーネントと下手に設計されたコンポーネントを区別している唯一の最も重要な要素は、内部データと実装の詳細を他のコンポーネントからどの程度隠蔽しているかである。うまく設計されたコンポーネントは、その実装のすべての詳細を隠蔽し、実装とAPIをはっきりと分離している。
情報隠蔽(カプセル化)は多くの理由で重要。コンポーネントを並行して開発できるため、情報隠蔽はシステム開発のスピードを向上させる。各クラスやメンバーをできる限りアクセスできないようにすべき。
publicクラスでは、publicフィールドではなく、アクセサーメソッドを使う
publicクラスの可変なフィールドを公開すべきではない。しかし、可変であろうと不変であろうと、パッケージプライベートのクラスかprivateのネストしたクラスであればフィールドを後悔することが望ましい場合がある。
可変性を最小限にする
不変クラスは、そのインスタンスが変更できないという単なるクラス。不変クラスは、設計・実装、使用が可変クラスよりも容易。不変クラスは誤りにくく安全。
クラスを不変にする5つの規則
- オブジェクトの状態を変更するためのメソッドを提供しない
- クラスを拡張できないようにする
- すべてのフィールドをfinalにする
- すべてのフィールドをprivateにする
- 可変コンポーネントに対する独占的アクセスを保証する
不変クラスの唯一の実質的欠点は、個々の異なる値に対して別々のオブジェクトを必要とする。
継承よりもコンポジションを選ぶ
継承はコードの再利用するための強力な方法だが、常に再利用のための最善の道具とはならない。不適切に使われると、継承はもろいソフトウェアを作り出してしまう。
サブクラスとスーパークラスの実装が同じでプログラマの管理下にある場合、パッケージ内の継承を使うのは安全。拡張のために設計されて、かつ拡張のために文書化されているクラスを拡張する場合にも、継承を使うのは安全。
しかし、パッケージをまたがって、普通の具象クラスから継承することは危険。
メソッド呼び出しとは異なり、継承はカプセル化を破る。(= サブクラスは適切に機能するために、スーパークラスの実装の詳細に依存する)
メソッドをオーバーライドすることで発生してしまう問題については、既存のクラスを拡張する代わりに、新たなクラスに既存のクラスのインスタンスを参照するprivateのフィールドを持たせることで解決できる。既存のクラスが新たなクラスの構成要素になるため、この設計はコンポジションと呼ばれる。
コンポジションではなく継承を使うと決める前に、「拡張しようと考えているクラスは、そのAPIに何らかの血管を持っていないか。もし、欠陥があれば、自分のクラスにその血管を伝播させていることに不安はないか。」を自問する必要がある。
継承のために設計および文書化する、でなければ継承を禁止する
クラスはメソッドのオーバーライドの影響を正確に文書化しなければならない。(= クラスはオーバーライド可能なメソッドの自己利用を文書化しなければならない)
継承のためにクラスを設計するのは大変な仕事である。クラスの自己利用をすべて文書化しなければならないし、一旦文書化したら、クラスが存在する限りそれを守らなければならない。サブクラスの必要性があるとわかっていない限り、クラスをfinalと宣言するかアクセスできるコンストラクタがないようにすることで、継承を禁止するのがおそらくよい。
抽象クラスよりもインターフェースを選ぶ
Javaは、複数の実装を許す型を定義するためにインターフェースと抽象クラスの二つの仕組みを提供している。この二つの相違は、抽象クラスで定義された型を実装するには、クラスはその抽象クラスのサブクラスでなければならないこと。
新たなインターフェースを実装するように既存のクラスを変更するのは容易。インターフェースはミックスインを定義するには理想的。インターフェースは、回想を持たない型フレームワークの構築を可能にしている。
将来のためにインターフェースを設計する
主にラムダを活用するために、多くの新たなデフォルトメソッドが、Java8の中核のコレクションインターフェースに追加された。
考えられるすべての実装のすべての不変式を維持するデフォルトメソッドを書くことは必ずしも可能ではない。デフォルトメソッドの存在は、インターフェースをエラーや警告なしでコンパイルできるかもしれないが、実行時に失敗することがある。
インターフェースがリリースされた後に、インターフェースの欠陥によっては修正が可能かもしれないが、そのことに期待すべきではない。
型を定義するためだけにインターフェースを使う
クラスがインターフェースが実装することで、そのインスタンスでクライアントは何ができるのかについてクラスは述べるべき。他の目的のためにインターフェースを定義するのは不適切。
この検査に合格しないインターフェースの種類の一つは、いわゆる定数インターフェース。定数インターフェースパターンは、インターフェースの下手な使い方である。これを避けるために、enum型やインスタンス化不可能なユーティリティクラスで定数を提供するのがよい。
タグ付きクラスよりもクラス階層を選ぶ
タグ付きクラスは、冗長で、誤りやすく、非効率なので、適切であることはほとんどない。明示的なタグフィールドを持つクラスを書きたくなったら、そのタグを取り除いてクラス階層で置き換えられないかを考えるのがよい。
非staticのメンバークラスよりもstaticのメンバークラスを選ぶ
ネストしたクラスは、他のクラス内に定義されたクラス。ネストしたクラスには、staticメンバークラス、非staticのメンバークラス、無名クラス、ローカルクラスの4種類がある。staticのメンバークラス以外は、内部クラスとして知られている。
ネストしたクラスの用途は次の通りである。
-
staticメンバークラス
- たまたまあるクラス内で宣言され、そのエンクロージングクラスのメンバーのすべてに、たとえそのメンバーがprivateと宣言されていてもアクセスできる通常のクラス。
- 使い方は、その外部クラスと一緒に使うと有用なpublicのヘルパークラスとしてである。
- staticと非staticのメンバークラスの構文的な唯一の相違点は、staticのメンバークラスはその宣言にstatic修飾子があること。
- 非staticのメンバークラスの個々のインスタンスは、それを含むクラスのエンクロージングインスタンスと暗黙に関連づけられている。
- エンクロージングインスタンスなしで、非staticのメンバークラスのインスタンスを生成することは不可能。
-
非staticのメンバークラス
- 使い方は、エンクロージングクラスのインスタンスの関係ないクラスのインスタンスとしてみなせるアダプターを定義すること。
-
private staticのメンバークラス
- 使い方は、エンクロージングクラスが表すオブジェクトの構成要素を表すこと。
-
無名クラス
- 無名クラスは名前を持たない。そのエンクロージングクラスのメンバーではない。
- 無名クラスが宣言された箇所以外で、無名クラスをインスタンス化できない。instanceof検査やクラスの名前を指定する必要がある処理はできない。
- 無名クラスよりもラムダが好ましい。
- 使い方は、staticファクトリメソッド内の実装において。
-
ローカルクラス
- ローカルクラスは、4種類のネストしたクラスの中で、おそらく最も使われてはいけない。
要約すると、4種類の異なるネストしたクラスがあり、それが異なる用途を持っている。
ネストしたクラスが、一つのメソッドの外からも見える必要があったり、メソッド内に問題なく入れるのに長すぎるならば、メンバークラスを使う。
メンバークラスの個々のインスタンスが、エンクロージングインスタンスへの参照が必要ならば、非staticにする。そうでなければstaticにする。
クラスがメソッド内に属しているべきであり、一箇所からのみインスタンスを生成する必要があり、そのクラスを特徴付ける型がすでに存在していれば、無名クラスにする。そうでなければ、ローカルクラスにする。
ソースファイルを単一のトップレベルのクラスに限定する
Javaコンパイラは、単一のソースファイルに複数のトップレベルのクラスを定義することを許すが、そうすることに何も利点はなく、重大なリスクがある。そのリスクは、一つのクラスに対して複数の定義の提供が可能になること。
トップレベルのクラスを、クラス名と同じ名前の別々のソースファイルへ分けることが、問題の解決である。
原型を使わない
一つ以上の型パラメータを宣言に持つクラスやインターフェースは、ジェネリッククラスやジェネリックインターフェース。
個々のジェネリック型は、クラス名やインターフェース名の後にアングルブラケットで囲んで、そのジェネリック型の仮型パラメータに対応する実型パラメータのリストからなるパラメータ化された型の集合を定義する。
無検査警告を取り除く
ジェネリクスを用いてプログラミングする際には、コンパイラの警告を多く目にします。無検査キャスト警告、無検査メソッド呼び出し警告、パラメータ化された可変引数型警告、無検査変換警告である。取り除けるすべての無検査警告を取り除くのがよい。
SuppressWarningアノテーションを、できる限り最小のスコープに対して使うのがよい。
配列よりもリストを選ぶ
配列は2つの重要な点で、ジェネリック型と異なっている。
-
配列は共変(covariant)でジェネリックは不変(invariant)
- SubがSuperのサブタイプならば、配列型 Sub[] が Super[] のサブタイプである
- List
は List のサブタイプでもなければスーパータイプでもない
-
配列は具象化されている
- 配列は実行時にその要素型を知っていて強制する。Long の配列に String を保存しようとすると ArrayStoreException がスローされる。
- ジェネリックはイレイジャで実装されている。コンパイル時のみに型制約を強制し、実行時には要素の型情報を廃棄する。
ジェネリック型を使う
ジェネリック型は、クライアントのコードでキャストが必要である型より、安全で使いやすい。
ジェネリックメソッドを使う
クラスをジェネリック化できるように、メソッドもジェネリック化できる。入力パラメータと戻り値をキャストすることをクライアントに要求するメソッドより、安全で使いやすい。
メソッドを型安全にするには、引数と戻り値に対する要素型を表す型パラメータを宣言する。
APIの柔軟性向上のために境界ワイルドカードを使う
最大の柔軟性のためには、プロデューサがコンシューマを表す入力パラメータに対してワイルドカード型を使うのがよい。
広く使われるタイプのライプラリを書くなら、ワイルドカード型を適切に使うことは、必須だと考えるべき。それは、プロデューサ-extends、コンシューマ-super(PECS)。そして、すべての比較可能なものとコンパレータは、コンシューマであることを忘れてない。
ジェネリックと可変長引数を注意して組み合わせる
可変長引数の目的は、クライアントがメソッドへ可変長の引数を渡せるようにすることだが、それは漏出抽象化。
ヒープ汚染は、パラメータ化された型の変数が、その型ではないオブジェクトを参照している場合に発生する。
SafeVarargsアノテーションは、メソッド作成者がそのメソッドが型安全であることを約束することである。この約束とは引き替えに、コンパイラは呼び出しが安全ではないかもしれないメソッドのユーザに警告しなくなる。
次の場合にジェネリック可変長引数メソッドは安全。
- 可変長パラメータ配列に何も保存していなくて、かつ、
- 信頼できないコードに対してその配列(あるいは複製を)参照できるようにしていない。
ジェネリック(あるいはパラメータ化された)可変長パラメータを持つメソッドを書くなら、最初にそのメソッドが型安全であるようにするべき。そして、使いやすいように@SafeVarargeアノテーションをつける。
型安全な異種コンテナを検討する
コレクションAPIで示されているジェネリックスの一般的な使い方は、コンテナごとに固定数の型パラメータに制限している。コンテナでなくキーに対して型パラメータを指定することでこの制約をさけられる。
このような型安全異種コンテナに対するキーとしてClassオブジェクトを使える。このように使われるClassオブジェクトは、型トークンと呼ばれる。