Effective Java
- Effective Java によせて
- 第1章 はじめに
- 第2章 オブジェクトの生成と消滅
- 第3章 すべてのオブジェクトに共通のメソッド
- 第4章 クラスとインターフェース
- 第5章 ジェネリックス
- 第6章 enum とアノテーション
- 第7章 ラムダとストリーム
- 第8章 メソッド
- 第9章 プログラミング一般
- 第10章 例外
- 第11章 並行性
- 第12章 シリアライズ
Effective Java によせて
- 言語は文法・語彙・慣用法を学ばないと natural に話せない。
- 前二者は習うけど、3つ目はなかなか習わないよね。
- プログラミング言語も一緒。
第1章 はじめに
第2章 オブジェクトの生成と消滅
コンストラクタの代わりに static ファクトリメソッドを検討する
- 命名
from
: 型変換of
: 集約valueOf
:from
やof
の代わりinstance
/getInstance
:create
/newInstance
: ↑とほぼ同じだが、毎回インスタンスを生成することを保証するget型名
: ファクトリメソッドを持つクラスと生成されるクラスが違う場合に明示するnew型名
: ↑のインスタンス生成バージョン型名
: ↑二つの簡略バージョン
- 長所
- 名前を付けられる。
new BigInteger(int,int,Random)
よりBigInteger.probablePrime
の方がわかりやすい。 - コンストラクタと違って、毎回インスタンスを生成する必要がない。定数返すとかもできる。
- サブタイプのオブジェクトを返せる。
- 入力パラメータに応じて別のクラスを返すこともできる。
- 無名クラス返すこともできる。
- 名前を付けられる。
- 短所
private
コンストラクタしかないと、サブクラスが作れない。- 不幸中の幸いで、継承よりもコンポジションを促すのにつながることもある。 cf.
Collection
- 不幸中の幸いで、継承よりもコンポジションを促すのにつながることもある。 cf.
- プログラマがファクトリメソッドを見つけるのが難しい。※ 確かに経験上、
javax.json.JsonObject
とかそうだった!- 前述の命名を守ることである程度解消
- まとめ
- コンストラクタとファクトリメソッドはそれぞれ長所・短所があるので、static ファクトリメソッドは一旦は検討しておこうね。
多くのコンストラクタパラメータに直面したときはビルダーを検討する
- テレスコーピングコンストラクタパターン:少なければいいけどね……
- JavaBeans パターン:set し忘れる可能性もあるし、mutation はスレッドセーフじゃない。しかもそれによる不具合がずっと後の処理で起こるかもしれない。
private のコンストラクタか enum 型でシングルトン特性を強制する
これはggればでてくる
private のコンストラクタでインスタンス化不可能を強制する
はい
資源を直接結び付けるよりも依存性注入を選ぶ
クラスの振る舞いが他の資源に依存するなら、private static final
な field として持ってしまうのではなくて、
外から注入するようにしようね、というやつ。拡張性とテスタビリティが段違い。
※ よく使うのがあるなら factory method で提供しちゃえばいいってことかな。
不必要なオブジェクトの生成を避ける
new String("hogehoge")
とかやるなよって話。
String.matches
とかも、内部的に Pattern
インスタンスを毎回生成してるらしいので、
たくさん呼ぶなら Pattern
を cache した方が良い。
使われなくなったオブジェクト参照を取り除く
例えば stack の内部実装で配列を使っている場合、pop された object のところは null で上書きしておかないと参照が生き残ってて GC が回収してくんない。
ファイナライザとクリーナーを避ける
危ないし遅いから AutoClosable
を代わりに使おうね。
ファイナライザ・クリーナーを使うのは、auto close のセーフティネットとしてならアリ。
あとネイティブピア(Java の object じゃないので GC に回収されない)の分を強制的に回収したいなら実装するしかない。
try-finally よりも try-with-resource を選ぶ
はい。
第3章 すべてのオブジェクトに共通のメソッド
equals をオーバーライドするときは一般契約に従う
はい。javadoc 読もうね!
しないときはどういうときかというと
Thread
とかの、本質的にすべてのインスタンスが一意の場合equals
を使う想定をしていない場合- 可視性が低くてそもそもいろんな人が使う想定がない場合
- スーパクラスの実装で十分な場合
equals をオーバーライドするときは、常に hashCode をオーバーライドする
はい。hashCode
が重いなら cache してもいいよ!
toString を常にオーバーライドする
はい。
close を注意してオーバーライドする
なんかよくわからんが Closable
を実装するクラス作りたいときないし別にいいか。
Comparable の実装を検討する
equals
と同じで一般契約に従おうね!
オーバフローと浮動小数点誤差に気を付ける。
第4章 クラスとインターフェース
クラスとメンバーへのアクセス可能性を最小限にする
はい。
public のクラスでは、public のフィールドではなく、アクセッサーメソッドを使う
はい。
可変性を最小限にする
はい。
class を final
にしなくても、constructor を全部 private
にすることで実は同じような効果が得られる。
継承よりもコンポジションを選ぶ
コンポジション + 転送(いわゆる委譲)をやった方がいいときが多い。
継承のために設計および文書化する、でなければ継承を禁止する
継承によりカプセル化は簡単に破壊され得る。したがって、継承可能メソッドは implSpec アノテーションによって実装要件を明示し、継承クラスはそれを遵守しなければならない。それができないなら継承はすべきでない。その場合、class を final
にするか、すべてのコンストラクタを private
にしてファクトリメソッドを追加するかのどちらかをしておかなければならない。
抽象クラスよりもインターフェースを選ぶ
抽象クラスは三角形、インターフェースは一点
つまりピンポイントで実装したいときはインターフェースが勝る
基本的にはインターフェース、default 実装を与えたいなら abstract
Interface を定義して、
template method pattern で実装する。
こうして作った抽象クラスの実装を骨格実装 (skeletal implementation) と呼ぶ。
所謂 mixin をやるのにはインターフェースが向いてる。
将来のためにインタフェースを設計する
default method によって、実行時にこける可能性がある。
ex) org.apache.commons.collections4.collection.SynchronizedCollection
同期処理をするが、 default の removeIf
は同期なんて知ったこっちゃないので ConcurrentModificationException
が起きる可能性がある。
なので、上記クラスは removeIf
を override しなきゃいけない(のに、していない)。
教訓:default method を追加するのは、compilation error は起こさないかもしれないが、論理的に実装クラスを破壊する可能性がある。
型を定義するためだけにインタフェースを使う
定数インタフェース:アンチパターン
解決策
- 定数が既存のクラス・インタフェースに強く結びついているなら、それらのクラス・インタフェースに宣言すべき
- そうでなく、列挙とみなせるなら enum を使うべき
- そうでもない場合、utility クラスを作るべき
タグ付きクラスよりもクラス階層を選ぶ
タグ付きクラス:内部 enum を持ってるやつ。継承と何も変わらないので継承使おうね、という話。
非 static のメンバークラスよりも static のメンバークラスを選ぶ
enclosing instance: 内部クラスから見た、外部クラスのインスタンス
enclosing instance にアクセスする必要がある場合のみ、非 static のメンバークラスを選ぶ。
ex) Collection
interface の実装は、イテレーションを内部クラスとして実装するために、非 static メンバークラスを一般に用いる。
ネストしたクラスが、一つのメソッドの外からも見える必要があったり、メソッド内に問題なく入れるのに長すぎるならば、メンバークラスを使う。
その際、メンバークラスの個々のインスタンスが enclosing instance への参照を持ちたいなら、非 static にする。それ以外は static にする。
クラスがメソッド内に属しているべきであって、インスタンス生成をする必要のある個所が一か所に特定できる場合、
既にそのクラスを特徴付けるクラスが宣言されているのであれば、無名クラスにする。
そうでなく、新たにクラスを宣言しなければならないのであれば、ローカルクラスにする。
ソースファイルを単一のトップレベルのクラスに限定する
単一のソースファイルに複数のトップレベルのクラスを入れない。
代わりに、 static の内部クラスとすべき。
第5章 ジェネリックス
原型を使わない
はい。
無検査警告を取り除く
はい。
どうしても取り除くことができず、警告を起こしているコードが型安全だと明確に示すことができるなら suppress warnings する。
suppress warnings の scope はなるべく狭く。method につけるのではなく local variable につける、とか。
で、なんで安全と言えるのかに関する comment を入れよう。
配列よりもリストを選ぶ
はい。
ジェネリック型を使う
new T[]
ができない!:new Object[]
として downcast するか、諦めて Object[]
を持つことにして、取り出す度に T
に downcast するかのどちらか。
ジェネリックメソッドを使う
generic singleton factory pattern: UnaryOperator#identity
とか。返すのは常に単位射で同じ instance だけど、compiler 上は parameterized なものとして扱いたいので。
再帰型境界 (recursive type bound): Comparable
を使うときによく E extends Comparable<E>
とか書くけど、あれ。
API の柔軟性向上のために境界ワイルドカードを使う
はい。
型パラメータが method 宣言中に一度しか現れないならそれをワイルドカードで置き換えられるはず。
ジェネリックスと可変長引数を注意して組み合わせる
ヒープ汚染:パラメータ化された方の変数が、その型ではないオブジェクトを参照している場合に発生する。コンパイラが自動生成したキャストが失敗する可能性があり、ジェネリック型システムの根本的な保証を破るものである。
配列自体が危険だよって話じゃね? Integer[]
を Object[]
に cast しちゃうと String
とかぶち込めるようになっちゃう、的な。
ヒープ汚染は伝播させてはいけない = 可変長引数で受け取ったときにそれは配列として変数に保存されるが、その参照を決してその method の外側に公開してはいけない、ということ。
- ジェネリック可変長引数メソッドが安全である条件
- 可変長パラメータ配列に何も保存していない。かつ
- 信頼できないコードに対してその配列(あるいは複製)を参照できるようにしていない。
型安全な異種コンテナを検討する
クラスリテラル:String.class
みたいなやつ。これを引数としてどこかに渡す場合、それは型情報を伝えるためであって、そうして渡されたクラスリテラルのことを型トークン (type token) と呼ぶ。
型安全異種コンテナ (typesafe heterogeneous container) :Map<Class<?>, Object>
を field に持ってるやつ。
asSubclass
とか cast
とかの method を駆使すると実行時に検査をすることで、compilation error とか警告とかなくコーディングすることができるようになる。エラー発生も早めることができてまあまあうれしいね。
第6章 enum とアノテーション
int 定数の代わりに enum を使う
int enum pattern: アンチパターン。int
じゃなくても String
でもだめ。
enum ごとに分岐してやる処理 -> 定数固有メソッド実装 (constant-specific method implementation) で行ける場合もある。特に、分岐を enum class 内に定義している場合。
戦略 enum (strategy enum) : 例えば、曜日を定義するんだけど、週末と平日を分けたい場合、inner enum として WEEKDAY
WEEKEND
を持つ enum を宣言。これを曜日 enum の constructor に渡すようにする。
序数の代わりにインスタンスフィールドを使う
ordinal
は使うなよ。はい。
ビットフィールドの代わりに EnumSet を使う
int enum パターンと似た話。Set<Enum>
を引数に受け取る method を作ることで解決する。
序数インデックスの代わりに EnumMap を使う
ordinal
使うなよ。はい。EnumMap
があるのでそれ使う。ネストすることもある。
拡張可能な enum をインタフェースで模倣する
はい。 コードの重複が出てきた場合は、ヘルパクラスか static なヘルパメソッドにカプセル化する。
命名パターンよりもアノテーションを選ぶ
テストメソッドは Test
prefix をつけるとかするのではなくて @Test
annotation をつけるようにデザインする。こういう annotation を marker annotation と呼ぶ。失敗すべきテストは、想定される例外 Class
を引数に取る annotation を作る。
配列を保持する annotation もいいが、 @Repeatable
を使用することもできる。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
常に Override アノテーションを使う
はい。
型を定義するためにマーカーインタフェースを使う
はい。Serializable
とか。型を定義したくないなら marker annotation でいい。
第7章 ラムダとストリーム
無名クラスよりもラムダを選ぶ
はい。 関数名・documentation が欠けるので、計算が自明じゃなかったり長かったりするならラムダはやめとく。
ラムダよりもメソッド参照を選ぶ
はい。
標準の関数型インタフェースを使う
はい。実装者に強く求める契約があるならそれ専用のを作ってもいい。
ストリームを注意して使う
はい。前の値が必要なときは Pair
とかに持たなきゃいけないけど、煩雑になりやすい。逆マッピングの利用とかも検討する。
ストリームで副作用のない関数を選ぶ
はい。
戻り値型として Stream よりも Collection を選ぶ
Collection
として扱いたい開発者もいるかもしれないから。
ストリームを並列化するときは注意を払う
見境なく Stream
を並列化しないこと。例えば、limit
が挟まってる場合に並列処理をすると、その要素を計算すべきかどうかが独立に決定できないために、無駄な計算がたくさん行われる。Stream#iterate
を使っている場合も、直前の要素まで iteration しないと次の要素計算できないから無駄計算がたくさん発生する。※ LinkedList
の random access の話と似てるなあ。
可変リダクション (mutable reduction) はオーバヘッドが高くつくので、並列化しても恩恵が少ない。
あとは副作用がある場合、並列化によって壊れる可能性もある。※ それは実装者が悪くね?
並列化の中で乱数が欲しい場合、SplittableRandom
を使うとかの工夫が必要な場合もある。
第8章 メソッド
パラメータの正当性を検査する
Object#requireNonNull
とかによる assertion。assertion は method の入り口でやるべきだよね。はい。
必要な場合、防衛的にコピーする
コンストラクタでコピーしたり、getter で見せるものはコピーにしておいて、参照を切っておいたり。はい。最悪 javadoc で逃げてもいい。
メソッドのシグニチャを注意深く設計する
- 標準ライブラリを参考に命名する
- 便利なメソッドを提供しすぎない。よっぽど idiom 的なものなら許す。
- 引数は 4 つ以下を目指す
- method を分割して、直交性によって解決する。
List
のsubList
indexOf
を開発者に組み合させることによって実現させるとか。
- method を分割して、直交性によって解決する。
オーバーロードを注意して使う
- 同じパラメータ数でオーバロードは避ける。異なる名前にする。
- 同じ引数で異なる関数型インタフェースを受け取るようにメソッドをオーバーロードをしない。
可変長引数を注意して使う
必須なら (int first, int… remainings) みたいな signature にする。はい。
null ではなく、空コレクションか空配列を返す
はい。
オプショナルを注意して返す
non-null 意識。はい。
instance field に Optional
は持つべきではない -> その field を省略した subclass を定義すべき場合がほとんど。でも組み合わせ爆発起きるならしょうがないね、ぐらいな感じ。
すべての公開 API 要素に対してドキュメントコメントを書く
class/interface/constructor/method/field に対して宣言の前に javadoc を書く。
書くべきもの
- 事前条件
- 事後条件
- 副作用
第9章 プログラミング一般
ローカル変数のスコープを最小限にする
method 短くしようね。はい。
従来の for ループよりも for-each ループを選ぶ
はい。以下の場合は使えない。
- 破壊的フィルタリング
- 変換
- 並列イテレーション
ライブラリを知り、ライブラリを使う
java.lang
java.util
java.io
ぐらいはちゃんと知っとくべき。新しい feature は追っかけようね。
正確な答えが必要ならば、float と double を避ける
はい。
ボクシングされた基本データよりも基本データ型を選ぶ
基本データ型の方が
- 速い
- 比較が容易で直観的
- null 安全
他の型が適切な場所では、文字列を避ける
はい。
文字列結合のパフォーマンスに用心する
はい。StringBuilder
使いな。
インタフェースでオブジェクトを参照する
はい。ArrayList
じゃなくて List
使いな。field の型の話ね。
リフレクションよりもインタフェースを選ぶ
リフレクションしていいのは service provider framework を作るときぐらい。他の場合は interface 刺しといて実装は後から注入しようね、という OOP の本懐で解決しようぜ。
リフレクションすると
- 長くなる
- 遅くなる
- 動的型付けみたいなもんなので危ない
ネイティブメソッドを注意して使う
はい。言われんでも使わんけどな……
注意して最適化する
最適化するな。でも、最適化の余地を潰すような設計はするな。
プログラムを書くときは性能度外視。
システム設計、とりわけ API、通信プロトコル、永続データ形式を設計しているときはパフォーマンスについて真剣に考える。作り終わって計測して速ければそれで終わり。遅ければプロファイラで見てボトルネック特定して、採用されてるアルゴリズム見て改善してく。
一般に受け入れられている命名規則を守る
はい。
第10章 例外
例外的状態にだけ例外を使う
まあ、はい。
例えば Iterator
は next
を持っているがこれは NoSuchElementException
を吐く可能性がある。一方で、そうなるかどうかを検査するための hasNext
がある。つまり、後者を併せて使用することで、client は例外ハンドリングを強制されることなく API を利用できる。
回復可能な状態にはチェックされる例外を、プログラミングエラーには実行時例外を使う
迷ったら実行時例外で。どっちでもないな、と思うなら例外定義自体やめようね。チェックされる例外を投げる場合、回復処理を助けるための method を定義しようね。
チェックされる例外を不必要に使うのを避ける
チェックされる例外は基本的には Optional
な戻り値で解決する。ただ、Optional
だとなんで失敗したかについての情報が不足する場合があるので、そういうときは使ってもいいよ。
※ vavr の Try
使えば?という話はある気がする。
標準的な例外を使う
使いまわせるなら使いまわそう。ただし semantics に気を付けて使う。詳細を追加したいときは extends
する。でも継承の乱用は悪。
抽象概念に適した例外をスローする
例外翻訳: 低レイヤで発生した例外を、高レイヤに対して別の例外に包んで投げなおすこと。乱用は避ける。実は assertion でいいんじゃないの?とか。
各メソッドがスローするすべての例外を文書化する
はい。javadoc 書こうね。多くの member method に共通してるようなものは class の javadoc に書いてもいい。
詳細メッセージにエラー記録情報を含める
はい。なんで例外出たの?がわかるようにしておこうね。traceability の話。逆に、security 的にヤバいものは含まないようにしようね。
エラーアトミック性に努める
エラーアトミック性: 例外が出たら rollback する。つまりその method が呼び出される前の状態に復元しておく。
これを達成できないなら、そう javadoc に書いておく。
例外を無視しない
握りつぶすなよーというやつ。try-catch を業務的な分岐に使うな、というやつね。
無視しても許容される場合として、既に必要な情報は読み取れたから中断する必要ないとか、そんな重要じゃない(例えば UI の色がちょっと変わるだけとか)とか、そういうのなら百歩譲っていいかな。でも握りつぶすのが正当であることは comment で示そうね。
第11章 並行性
共有された可変データへのアクセスを同期する
Thread#stop
使うな。
ある変数への参照を同期したい場合、その変数への書き込み method と読み出し method の両方を synchronized する必要がある。
過剰な同期は避ける
同期の最中に異質な method の呼び出しは避ける。
異質: client が override できるような、中身の知れないような method のこと。データを壊したり、デッドロックを起こしたり、そういう実装が client 次第で可能であるから、呼び出さない方が良い。
スレッドよりもエグゼキュータ、タスク、ストリームを選ぶ
自分でスレッドを立てるよりも、エグゼキュータサービスにタスクを食わせる方がよかったりする。
wait と notify よりも並行処理ユーティリティを選ぶ
concurrent collection 使おうね。
スレッド安全性を文書化する
遅延初期化を注意して使う
スレッドスケジューラに依存しない
第12章 シリアライズ
Java のシリアライズよりも代替手段を選ぶ
JSON とか protobuf とかで代替する。やるとしてもホワイトリスト形式で、信頼できない出所のデータストリームを deserialize しないこと。
Serializable を細心の注意を払って実装する
serialVersionUID
を宣言せねばならず、これを自動生成しようとすると、クラスの中身を変えるたびに変わっちゃって後方互換性がなくなる。- bug とかセキュリティホールの可能性が大きくなる。
- テスト負荷増大
なので基本的に Serializable
やめた方がいいねって話。
カスタムシリアライズ形式の使用を検討する
防御的に readObject メソッドを書く
インスタンス制御に対しては readResolve よりも enum 型を選ぶ
シリアライズされたインスタンスの代わりに、シリアライズ・プロキシを検討する
シリアライズプロキシパターンというのがあるらしい。へー。