kawasin73 / knowledge

気になったツールやサイト、勉強した内容をまとめます。
9 stars 0 forks source link

本 : Effective Java 第3版 #33

Closed kawasin73 closed 3 years ago

kawasin73 commented 4 years ago

amazon

IMG_7647

kawasin73 commented 4 years ago

2章 オブジェクトの生成と消滅

item 1 : コンストラクタの代わりに static ファクトリメソッドを検討する

static factory method 自分自身を返り値にする public な static method

メリット

interface に static method を定義できるようになったのはJava 8 から。それ以前はインスタンス化不可能なクラスに static メソッドをつけていた

デメリット

サービスプロバイダフレームワーク

Go の sql データベースのフレームワークとか

Java 6 以降は、java.util.ServiceLoader がサービスプロバイダフレームワークとして使える

item 2 : 多くのコンストラクタパラメータに直面したときにはビルダーを検討する

パラメータが多いと、コンストラクタも static ファクトリメソッドも使いづらい

デメリット

item 3 : private のコンストラクタか enum 型でシングルトン特性を強制する

シングルトンにするとテストが大変。

実現方法

private なコンストラクタであっても AccessibleObject.setAccessible メソッドを使ってリクフレクションにより呼び出すことができてしまう。防ぐためにはコンストラクタでチェックして例外を throw する。

enum を使ってシングルトンを提供すると便利。Serializable やリフレクションの問題は存在しない。ただし、あるクラスを継承したシングルトンは enum にはできない。

item 4 : private のコンストラクタでインスタンス化不可能を強制する

static なフィールドとメソッドのみを提供したいとき。 関数をグルーピングしたいときなどに使える

インスタンス化することは想定していないことを明示する

public class UtilityClass {
  private UtilityClass() {
    throw new AssertionError();
  }
}

コメントをつけないと混乱する可能性がある

final について

https://qiita.com/ryo2132/items/eb9a63f2b107c1d6b25c

item 5 : 資源を直接結びつけるよりも依存性注入を選ぶ

静的なユーティリティクラスとシングルトンは、下層の資源でパラメータ化された振る舞いを持つクラスに対しては不適です。

コンストラクタで依存先を注入する。

メリット

依存資源としてファクトリオブジェクトを渡す応用もある。Java にはファクトリとして Supplier<T> インターフェースがある。

item 6 : 不必要なオブジェクトの生成を避ける

immutable であると再利用しやすい。

文字列の初期化で new String() で生成するとオブジェクト生成コストがかかる。単に ”hello” と生成すると同じオブジェクトを使い回す。

static factory method と相性がいい。 初期化コストが高いものはキャッシュして使い回す方がいい

lazy initialization はそれほど効果がないらしい?

自動ボクシング (autoboxing) : primitive がボクシングされたデータ型に変換されてしまうこと

longLong がボクシングしている Longlong を足そうとすると、longLong に変換されてオブジェクト生成が発生する。

ただし、JVM でのオブジェクト生成のコストは小さいものと考えたほうがいい。初期化処理が小さいオブジェクトは、自分でオブジェクトプールを管理するより JVM に任せたほうがいい。

item 50 では防御的コピーを取り上げる。これはこことは真逆。

item 7 : 使われなくなったオブジェクト参照を取り除く

obsolete reference : 使われなくなった参照

連鎖的に参照が残ってしまって大きなメモリリークに繋がることも。

解決策:使い終わったら明示的に null を代入して参照を外す。

メモリ管理が間違っていたときに NullPointerException で早期に発見できる。

通常は null を設定するのではなく変数をスコープ外に出すだけで十分。逆に null を代入して回るのは複雑になりがち。

原因

Heap profiler などを使って見つけることになる。

item 8 : ファイナライザとクリーナーを避ける

finalizer は予想不可能であり危険。 クリーナーは Java 9 から導入された finalizer の代わり。クリーナーは独自のクリーナースレッドを指定できる。 メモリ以外の資源の回収のためには、try-with-resourcestry-finally を使う。

ファイナライザのダメなところ

ファイルなどの資源の終了処理は、 AutoCloseable をクラスに実装する。ただし、2重 close を防ぐ( IllegalStateException )などの対応は必要。 これを try-with-resources ブロックから使ってもらう。

ファイナライザやクリーナーの使い道

close メソッドの呼び忘れに対するセーフティネット。ただしコスト的に見合うかを検討する必要はある。

ネイティブピア(Java が管理しないネイティブのオブジェクト)の開放。重要性が高くない場合は finalizer を使うのもあり。

クリーナーを使うときは、循環参照を避けるために Runner を static なクラスにする必要がある。

item 9 : try-finally よりも try-with-resources を選ぶ

try-finally はうまくいかない。2つ目のリソースがあるとネストしてしまい見通しが悪い。

Java 7 で try-with-resources 文が導入された。 AutoCloseable インターフェイスを実装すると対応できる。

try (InputStream in = new FileInputStream(src);
     OutputStream out = new FileOutputStream(dest)) {
  // ...
}

複数のリソースの確保にも対応している。 ログの隠蔽問題についてもスタックとレースに隠蔽された表記が追加されるため、わかりやすくなる。

kawasin73 commented 4 years ago

3章 すべてのオブジェクトに共通のメソッド

Object のオーバーライドされる前提のメソッド

item 10 : equals をオーバーライドするときは一般契約に従う

オーバーライドしないときはインスタンス自身とのみ equals は true になる

オーバーライドする必要のない場合

equals は論理的等価性の概念を持っているときに実装する。一般には値クラス

equals メソッドが実装する同値関係

インスタンス化可能なクラスを拡張して equals の契約を守ったまま値要素を追加する方法はない

つまり、equals を一回 override したクラスを継承してもう一度 override はできないということ。

回避策としては継承を使わずにコンポジションを使う。

java.sql.Timestampjava.util.Date を拡張しているがここでの対称性を守れていないので混ぜると危険。

高品質の equals メソッドのコツ

対称的、推移的、整合的 の3つを保証する単体テストを書く

注意事項

大抵は、Google の AutoValue を使えば自動生成できる。

TODO: AutoValue

item 11 : equals をオーバーライドするときは、常に hashCode をオーバーライドする

HashMap や HashSet でうまく動かなくなる

hashCode の一般契約

ハッシュ化の方法はいくつかある。31 を掛ける方法など。(31 の掛け算は JVM では最適化されてたりする) 衝突の少ないハッシュ関数は、com.google.common.hash.Hashing を参照。 Objects.hash() 関数はパフォーマンスは良くないが簡単に利用できる。 計算コストが高い場合はキャッシュ化も考える

hashCode の内部実装に依存させないために仕様を定義しない。それによって利用側に柔軟性を強制できる

item 12 : toString を常にオーバーライドする

toString には全ての興味のある情報を含めるべき。

toString の結果の形式を定義する場合は、文字列からインスタンスを生成する static ファクトリメソッドを作ると便利。一方で定義すると変更できなくなってしまう。 形式を定義する場合でもしない場合でもその旨をコメントに明記する。

item 4 の static ユーティリティクラスには不要。enum もデフォルトのものが優秀なので新たに定義する必要はない。

item 13 : clone を注意してオーバーライドする

Cloneable インターフェイスはあるが、失敗している。clone は Object の protected メソッドである。 が、clone は使われているので理解する必要がある。

慣習で、clone は super.clone を呼び出して得るべき

clone の返り値の型はそのサブクラスにできる。(共変戻り値型 / covariant return type)

super.clone は、Cloneable を実装していない時には、CloneNotSupportedException を発生させるので、try-catch が必要。ただし、catch はされないことが前提となる。

内部で可変な参照を持っているときは、参照先も clone することを気をつける。再代入になるので final が使えない

public な clone メソッドを作る場合は、throw も外すと良い

他の手段として、コピーコンストラクタ、コピーファクトリがある。この形式では HashSet を TreeSet に変換する変換コンストラクタなども実現できる。

配列は、clone を使うべきであるが、それ以外では推奨されない。

item 14 : Comparable の実装を検討する

Comparable インターフェイスには、compareTo がある。順序を表し、ソートなどの既存のアルゴリズムに簡単に統合できるようになる。

一般契約

クラスをまたがって動作しないので equals に比べれば楽。equals と同じように継承して override するとうまく動かない。

Float や Double などの基本型では、 <, > ではなく compare を使う

コンパレータ構築メソッド(comparator construction method)を使うと簡潔に実装できる。

引き算で compareTo を実装することは整数のオーバーフローや浮動小数点数算術の副作用の危険があるため推奨されない。Integer.compare か、Comparator を使う。

kawasin73 commented 4 years ago

4章 クラスとインターフェース

item 15 : クラスとメンバーへのアクセス可能性を最小限にする

情報の隠蔽、カプセル化を実現する。 外部に公開することが少ないと、内部の実装を変更しても外部への影響が小さくなる(最適化が容易になる)。

トップレベルのクラス、インターフェイスでは、public をつけた時のみパブリックになる。それ以外はパッケージプライベート。

パッケージプライベートなクラスが1つのクラスからのみ利用されている場合は、ネストしたクラスにすることで可視性を狭められる。

基本的に全て private にして、うまくいかない時に適切なレベルを選ぶ。 Serializable を実装するとプライベートなフィールドが公開 API の中に漏れてしまう。

サブクラスのメソッドはスーパークラスの可視性を狭めてはいけない。コンパイルエラーになる。

テスト容易性のために private をパッケージプライベートにしてもいいが、それ以上の公開は許されない。

インスタンスフィールドは public にするべきではない。外部から変更されてスレッドセーフではなくなる。また型の変更ができなくなる

static フィールドも同様に public にするべきではないが、大文字+スネークケースで構成される定数は例外

配列の中身は可変であることに注意。基本的に public にするべきではない。

Java 9 からはモジュールシステムが追加されている。モジュールはパッケージをグループ化する仕組み。モジュール内の公開されていない public と protected はモジュール外からはアクセスできない。

item 16 : public のクラスでは、public のフィールドでハンク、アクセッサーメソッドを使う

final であれば害は少ない。

item 17 : 可変性を最小限にする

Immutable なクラスを提供するための5つのクラス

操作の結果は関数の返り値として新しいオブジェクトが生成されて返ってくる。関数的な方法。 手続き的な方法ではない。関数名が動詞ではなく、前置詞である。

可変クラスは、状態遷移をするため管理が複雑になる。不変クラスはスレッドセーフになる。不変で共有できるため、キャッシュもできる。

コピーしても全く同じものがコピーされるだけなので意味がない。防御的コピー、clone メソッド、コピーコンストラクタは必要ない。

不変クラスの実装で内部表現を共有することもできる。

欠点は、個々の異なる値に対して別のオブジェクトを生成してしまうこと。コストの大きな変換処理ではステップごとにオブジェクト生成してしまう。 解決策1:複数ステップを1ステップにまとめた変換メソッドを提供 解決策2:public の可変コンパニオンクラス(StringBuilder など)

パフォーマンスのために、externally visible な変更をしないという制約のもとで上の5つの規則を緩めることができる。(内部での一貫性のあるキャッシュなど)

基本的にフィールドは、private final にすることが望ましい。

item 18 : 継承よりもコンポジションを選ぶ

パッケージをまたがって、具象クラスから継承することは危険。メソッド呼び出しとは異なり、継承はカプセル化を破る。サブクラスはスーパークラスの実装に依存するため、スーパークラスの実装の変更に弱い。新しいメソッドがスーパークラスに追加された時に実装もれが発生したりする。

コンポジションにして、メソッド呼び出しを forwarding する。

forwarding クラスを実装すると使いまわせる。

用語:wrapper, decorator pattern, delegation

ラッパークラスの欠点:callback framework には向いていない。子クラスはラッパーの存在を知らないので自分自身を登録してラッパーを回避してしまう。(SELF 問題)

メソッド呼び出しのオーバーヘッドやラッパーのオーバーヘッドは大きな問題にはならない。

subclass は subtype である。is-a 関係が成り立っていない時はコンポジションを使う。Java では Stack-Vector や Properties-Hashtable は間違っている。

継承によってスーパークラスの API を引き継ぐことになる。APIが欠陥を持っているときにその欠陥が伝播させられる。

item 19 : 継承のために設計及び文書化する、でなければ継承を禁止する

item 20 : 抽象クラスよりもインターフェースを選ぶ

Java 8 で default メソッドが導入され、abstract クラスと interface は同じ機能を持つ。ただし、Java は単一継承なので抽象クラスは使いづらい。

interface の制約を回避するために、抽象骨格実装(skeletal implementation) クラスは interface と abstract クラスの長所を組み合わせる。Template Method パターン。 命名の慣習として、Abstract<Interface_name> がよく使われる。

item 21 : 将来のためにインターフェースを設計する

Java8 より前は default メソッドがなかったので interface へのメソッドの追加は即コンパイルエラー。デフォルトメソッドでメソッドの追加は可能になったが、全ての継承された先において安全な実装であるとは言えない。

デフォルトメソッドによって interface へのメソッドの追加は可能であるが、避けるべきで、最初に慎重に設計する方が大事。

item 22 : 型を定義するためだけにインターフェースを使う

定数インターフェイス(メソッドのないインターフェイス)はアンチパターン。インターフェイスの趣旨に反しているから java.io.ObjectStreamConstants は例外

定数は、クラスや enum、ユーティリティクラスなどで定数は提供するべき。

数値の中の _ は無視されるから見やすくするために使うのが良い。

item 23 : タグ付きクラスよりもクラス階層を選ぶ

タグ付きクラスは、内部に type などの具象を表すフラグを持っておき、switch 文などで動作を分岐するようなクラス。 サブタイプを使うことでわかりやすく、効率的になる。

item 24 : 非 static のメンバークラスよりも static のメンバークラスを選ぶ

nested class は4種類

item 25 : ソースファイルを単一のトップレベルのクラスに限定する

Java の仕様としては可能だが、重複定義された時の挙動は未定義。 わざわざややこしいことはしない!

kawasin73 commented 4 years ago

5章 ジェネリックス

Java 5 以降でジェネリクスが使える ジェネリクスがキャストよりもいい点は、エラーが実行時ではなくコンパイル時に発生すること

item 26 : 原型を使わない

1つ以上の型パラメータを持つクラスやインターフェイスを、ジェネリッククラス、ジェネリックインターフェイスと呼ぶ。まとめてジェネリック型。

List<E> -> List<String> では

要素型がわからないような場合は、原型ではなく、unbounded wildcard type (非境界ワイルドカード型) を使う。 List<?>

Collection<?> には null 以外のオブジェクトを入れることができない。read only? によって型を守っている?

クラスリテラルでは原型を使わないといけない。クラスリテラル (List.class) にパラメータ型は使えない。

instanceof でも原型を使うことが望ましい。ただし型検査をした後は、Set<?> などの非境界ワイルドカード型にキャストして使う。

item 27 : 無検査警告を取り除く

頑張って無検査警告を解決していこう。

diamond operator (ダイアモンド演算子) <> で型推論がされる。

確実に安全で警告を取り除けないときは、 @SuppressWarnings("unchecked") アノテーションをつける。ただし最小のスコープで。return 文は宣言ではないのでつけられないから、ローカル変数を宣言してつける。 コメントをつけることも重要。

item 28 : 配列よりもリストを選ぶ

ジェネリック配列の生成はコンパイルエラーになる。可変長引数では配列が生成されるので注意が必要。解決策もある。

配列よりもコレクション型の List を使った方がいい。

配列とコレクション型を一緒に使おうとするとコンパイルエラーや警告が発生する。その時は配列をリストに変換すると良い。(パフォーマンスは若干劣化するが安全になる)

item 29 : ジェネリック型を使う

Objectを扱いキャスト前提であるクラスを後から互換性を保ったままジェネリック型に変換できる。

Object 型をパラメータ型に置き換える。エラーに対処していく。 E[] の生成でエラーになるときは、Object[] を生成して E[] にキャストするか、Object[] で保持して利用時に E にキャストするか。

ジェネリック型の中で private であれば配列を使うのもあり。

型パラメータに基本型(int, float など)を使うことはできず、ボクシングした型を使う

item 30 : ジェネリックメソッドを使う

メソッドでの型パラメータの宣言は、メソッドの修飾子と戻り値型の間に。

ジェネリックシングルトンファクトリ : 恒等関数の生成などで使い回しを表現するときに便利

recursive type bound (再帰型境界) : <E extends Comparable<E>>

item 31 : API の柔軟性向上のために境界ワイルドカードを使う

パラメータ化された型は不変である。時々これが不自由になる時がある。

bounded wildcard type (境界ワイルドカード型) : Iterable<? extends E> E のサブクラスをパラメータ型にとる Iterable, Iterable<? super E> E が継承しているスーパークラスをパラメータ型にとる Iterable

PECS : producer - extends , consumer - super Get&Put 原則

注意点:戻り値型として境界ワイルドカード型を使わない。

明示的型引数(Java 8 より前では必要)

Set<Number> numbers = Union.<Number>union(integers, doubles);

型パラメータがメソッド宣言中に1度しか現れない時は、ワイルドカードで置き換えることができる。(API がシンプルになる) 一方で、List<?> へは代入ができないので型パラメータを使った private メソッドで処理をする。(逆に複雑なような気もする。)それによって API は綺麗になる。広く使われるような API では特に有効

item 32 : ジェネリックスと可変長引数を注意して組み合わせる

ジェネリックスと可変長引数は Java 5 で同時に追加されたが協調しない。

本来はジェネリックの可変長引数はコンパイルエラーにするべきだが、利便性が高いため Java はこの不整合を受け入れている。

@SafeVarargs アノテーションをメソッドにつけることで型安全であることを明示する。パラメータ化された型の可変長引数を持つメソッドでは必ずつけるようにする。 static か final か private でのみ使える。オーバーライドされないために

安全であるためには以下が必要

ジェネリクスのパラメータ配列はコンパイル時は Object[] に割り当てられるため、バグの温床になる。

代替手段として、可変長パラメータではなくリストを受け取るようにする。

item 33 : 型安全な異種コンテナを検討する

ジェネリクスの主な用途 : Set や Map などのコレクション、ThreadLocal, AtomicReference などの単一要素コンテナ

クラスリテラル (Class<String> , String.class で得られる)をキーとして使う。

型安全異種コンテナ(typesafe heterogeneous container)

public class Favorites {
  private Map<Class<?>, Object> favorites = new HashMap<>();
  public <T> void putFavorite(Class<T> type, T instance) {
    favorites.put(Objects.requireNonNull(type), instance);
  }
  public <T> T getFavorite(Class<T> type) {
    return type.cast(favorites.get(type));
  }
}

type.cast() で動的キャストが可能。 この Favorites は原型を使うことで壊れるが、無検査警告がコンパイル時に出るので気づける。put 時に type.cast(instance) で検査することで実行時型安全検査ができ、原型に対処できる。

具象化不可能型には使えない。List<String>.class は文法エラー。

境界型トークンを使うことで型を制約することができる。 Class クラスは asSubclass でキャストできる。

kawasin73 commented 4 years ago

6章 enum とアノテーション

item 34 : int 定数の代わりに enum を使う

列挙型 (enumerated type) : 固定数の定数から成り立つ型

enum が Java に追加される前は、int enum パターンが使われていた。しかし、型安全ではない、変更に弱い、デバッグが辛い、名前空間がないなどのデメリットが多い。

String enum パターンもある。ハードコードされると typo に気づけないなどさらに悪手。

大人しく enum 型を使おう。

enum はクラスとして実装されており、それぞれの値はシングルトンの public static final 定数。 別の値を紛れ込ませることができない。

enum にはメソッドを追加できる。コンストラクタを定義してフィールド値を設定することもできる。ただし immutable にするのが望ましい。

values() メソッドで宣言されている順序で配列を受け取れる。

値を減らした時は、それを使っていないクライアントプログラムは問題ないが、使っているプログラムはリコンパイルでエラーになる。

値によって振る舞いを変えたいときに switch(this) 文で切り替えることは、throw AssertionError が必要な上に変更に弱い。constant-specific class body (定数固有クラス本体) を持つことで定数ごとにメソッドをオーバーライドして定義する。constant-specific method implemetation (定数固有メソッド実装)と呼ばれる。オーバーライドされるメソッドは抽象メソッドとして enum 内に定義する。

enum は定数名を定数自身へ変換する valueOf(String) メソッドを自動生成する。 toString を実装するなら fromString も実装することを検討する。

enum のコンストラクタは定数変数を除いて static フィールドへアクセスできない。(初期化されていないから)enum のコンストラクタから他の enum にアクセスできないので注意。

複数の重複する振る舞いがあるメソッドは定数固有メソッド実装では冗長。 strategy enum (戦略 enum) を使うとエレガントに解決できる。同じ振る舞いをするグループを別の enum で定義してそちらに処理を移譲し、元の enum のデータ定義でグループを定義する。

外部の enum などに対しては switch 文を使う。

item 35 : 序数の代わりにインスタンスフィールドを使う

enum の実体は int 値と関連づいており、ordinal() メソッドで取得できるが、これに依存すると保守が大変。代わりにインスタンスフィールドに定義することで保守が楽になる。

item 36 : ビットフィールドの代わりに EnumSet を使う

ビットフィールドは、ビット和操作によって集合を表せるので便利だが、int enum 以上のデメリットがある。ビット幅を固定するので変更ができない、printable でない。

java.util.EnumSet が解決する。Set インタフェースを実装している。内部ではビットベクトルを保持している。

java 9 の時点では immutable な EnumSet に対応していないのが弱点

item 37 : 序数インデックスの代わりに EnumMap を使う

配列のインデックスに ordinal() の値を使うのは良くない。 配列はジェネリックスとの相性が悪い。数値はラベルを手動でつけないといけない。誤った int 値の利用は実装者の責任。

java.util.EnumMap では enum をキーに使える。EnumMap は内部的には配列を使っているので十分速い。使うときはコンストラクタに Class オブジェクトを渡す必要がある。

ストリーミングでは、groupingBy に EnumMap を組み合わせることで最適化できる。

item 38 : 拡張可能な enum をインタフェースで模倣する

enum 型を外部から直接拡張するすることはできないし、継承して拡張することもできない。

共通した interface を実装した enum を用いることで同じ API で使えるため enum を拡張できるようになる。

enum の継承はできないのでメソッドの共有はできないが、interface のデフォルトメソッドとして定義することで重複を防ぐことができる。

item 39 : 命名パターンよりもアノテーションを選ぶ

命名パターンの欠点

アノテーションで解決する。JUnit はリリース 4 から採用。

メタアノテーション:アノテーションに対するアノテーション

パラメータなしの static のメソッドに対してのみ付与することは強制できないためコメントに書いている。強制するためにはアノテーションプロセッサを書く必要がある。(javax.annotation.processing)

パラメータを持たないアノテーションは、マーカーアノテーション。

リフレクションができる。Method.invoke() でメソッドを実行。実行中のエラーは InvocationTargetException に Wrap されて送出される。

アノテーションインタフェースに value() を指定することでパラメータを受け取れる。 コンパイル時にはアノテーションパラメータは正しかったけど、実行時に例外型を表すクラスファイルがなかった場合、TypeNotPresentException が発生する。 アノテーションパラメータは配列にすることで複数受け取れる。設定する時は単一要素を指定することもできるし、{}でカンマ区切りを囲って複数指定もできる。

@Repeatable にコンテナアノテーション型を指定することで同じアノテーションを同時に複数設定できるようになる。 getAnnotationsByType() では正しく振舞うが、isAnnotationPresent() は単一のアノテーション型をコンテナアノテーション型を区別し、コンテナアノテーション型であるかどうかをチェックするので正しく振る舞わない。

item 40 : 常に Override アノテーションを使う

@Override をつけると、オーバーライドではなくオーバーロードしてしまった バグにコンパイラが気づく。

オーバーライドするメソッドが抽象クラスの抽象メソッドの場合は Override をつける必要はない。 インターフェイスのメソッドの実装に @Override をつけるのも良い。

item 41 : 型を定義するためにマーカーインタフェースを使う

マーカーインタフェース : メソッドを持たない interface

マーカーインタフェースのマーカーアノテーションにない長所

マーカーアノテーションの長所

kawasin73 commented 4 years ago

7章 ラムダとストリーム

Java 8 で関数型インタフェース、ラムダ、メソッド参照が追加された

item 42 無名クラスよりもラムダを選ぶ

単一の抽象メソッドを持つインターフェイスを無名クラスとして使って関数オブジェクトを表していた。これを関数型インタフェースと呼ぶ。 無名クラスはラムダ式で置き換えることができる。

ラムダでは型は省略できる。型を明示することでプログラムが明瞭になるわけでなければ型は省略するのが良い。コンパイラが型推論できなかった時に型をつける。

enum の定数固有クラス本体でのメソッド定義よりも、ラムダを使った enum インスタンスフィールドの方が簡潔に表せる。ただし、式の中身が簡潔な場合。また、enum のコンストラクタからは enum のインスタンスフィールドにアクセスできない。

ラムダでは1行が理想。長くても3行。

ラムダは抽象クラス、複数のメソッドを持つインタフェースには対応していない。無名クラスが対応している。ラムダでの this はエンクロージングインスタンスを表す。

ラムダはシリアライズを確実に行えない。

item 43 : ラムダよりもメソッド参照を選ぶ

メソッド参照はラムダよりも簡潔な関数オブジェクト生成方法。 ラムダにできなくてメソッド参照にできることはない。

メソッド参照の種類

メソッド参照の種類 同等のラムダ
static Integer::parseInt str -> Integer.parseInt(str)
バウンド Instant.now()::isAfter Instant then = Instant.now(); t -> then.isAfter(t)
アンバウンド String::toLowerCase str -> str.toLowerCase()
クラスコンストラクタ TreeMap<K,V>::new () -> new TreeMap<K,V>()
配列コンストラクタ int[]::new len -> new int[len]

item 44 : 標準の関数型インタフェースを使う

ラムダなどの関数オブジェクトを受け入れるために独自のインタフェースを定義するのではなく、標準の java.util.function パッケージに定義してあるインタフェースを利用する。

全部で 43 個定義されているが、以下の 6 個の基本インタフェースを覚えれば応用できる

インタフェース 関数のシグニチャ
UnaryOperator<T> T apply(T t) String::toLowerCase
BinaryOperator<T> T apply(T t1, T t2) BigInteger::add
Predicate<T> boolean test(T t) Collection::isEmpty
Function<T,R> R apply(T t) Arrays::asList
Supplier<T> T get() Instant::now
Consumer<T> void accept(T T) System.out::println

それぞれに基本データ型の int long double について派生型がある

2 個の引数をとる BiPredicate<T,U>, BiFunction<T,U,R>, BiConsumer<T,U> がある。

Supplier には boolean を返す BooleanSupplier がある

ボクシングされた基本データの関数型インターフェースではなく、基本データ型の関数型インターフェイスを使うことが望ましい。大量のデータをボクシングすることはパフォーマンスに良くない

あえて標準の関数型インタフェースを使うのではなく独自に宣言した方がいい場合(例 Comparator<T>ToIntBiFunction<T,T>

@FunctionalInterface アノテーションによって関数型インタフェースであることを明示できる

関数型インタフェースを使ったメソッドを定義するときは、同じ引数の位置に関数型インタフェースを定義するメソッドを複数定義すると使う側が不便。

item 45 : ストリームを注意して使う

ストリームパイプラインには中間操作と終端操作がある。

ストリームパイプラインは遅延して評価される(lazily)。評価は終端操作が呼び出されるまで開始されないし、終端操作を完了させるために必要のないデータ要素は計算されない。

ストリームパイプラインは parallel メソッドを呼び出すと並列実行される。

コードブロック(ループ)にできてラムダ(関数オブジェクト)にできないこと

ストリームが得意なこと

ストリームでは値を別の値にマッピングすると古い値は失われるので複数のステージの値を使う処理には向いていない。元の値を復元できる場合は対処できる。

ストリーム要素の変数名は複数名詞が望ましい。

ぶっちゃけ、ループを使うかストリームを使うかは好み

item 46 : ストリームで副作用のない関数を選ぶ

Collectors を使おう!

純粋関数 : 結果が入力だけに依存している関数

forEach はストリームの計算結果を表示する処理に使うべき

collect() によって Collection に変換できる。コレクターは toList(), toSet(), toCollection(collectionFactory)。最後のは独自のコレクションを設定するために使う。コレクターに様々な条件を定義して Collection を生成する。

読みやすくするために、Collectors の全てのメンバーを static import するのが慣習

Collectors の 36 このメソッドのほとんどはマップへ集約するためのもの。

toMap() は一番シンプルだがキーが重複した時に IllegalStateException をスローする。それを防ぐために様々なマージ方法がある。 3つの引数がある場合は、3つ目の BinaryOperator がマージ結果を返す。 4つの引数がある場合、4つ目の引数は特定のマップ実装の利用を指定する。

groupingBy()は分類関数に基づくカテゴリーごとにグループ化したマップに変換する。

引数が1つのシンプルな場合、マップの値はリスト。 値を変えたい時は、ダウンストリームコレクターを第2引数に指定する。counting() は個数に変換する。ただし、counting() はダウンストリームコレクターとしての利用のみを想定している。そのほか様々なダウンストリームコレクターがある。 第3引数にはマップファクトリを指定できる。

item 47 : 戻り値型として Stream よりも Collection を選ぶ

Stream は for-each ループとも合わせて利用されることを念頭におく必要がある。 Stream は Iterable に定義されるメソッドを全て定義しているので、 Iterable を extend することができない。そのため、for-each との連携は複雑になる。

public static <E> Iterable<E> iterableOf(Stream<E> stream) {
  return stream::iterator;
}

逆に Iterable から Stream に変換することも面倒臭い

public static <E> Stream<E> streamOf(Iterable<E> iterable) {
  return StreamSupport.stream(iterable.spliterator(), false);
}

Collection インタフェースは Iterable のサブタイプでかつ、stream メソッドを持っているので、Stream と for-each の両方に対応できる。 public メソッドの返り値としては Collection が好ましい。

ただし、シーケンスがメモリに収まらないような場合には AbstractList を利用してコレクションを実装することも検討する。

コレクションが無理なら自然な方を返す。

item 48 : ストリームを並列化するときは注意を払う

CPU 使用率が跳ね上がって高止まりして処理は進まない : 活性エラー (liveness failure)

パイプラインの並列化がパフォーマンスの向上につながらない場合

並列で実行する場合は余分に要素を処理して必要のない結果は捨てることで limit と共存する

見境なくストリームパイプラインを並列化しない。

並列化によるパフォーマンスの向上が得られるのはサブレンジへの分割が低いコストでできる以下の要素

また、これらの要素は順次処理で参照の局所性がある

終端処理が重い、順次行われる場合は並列化の恩恵は限定的。

独自の Stream, Iterable, Collection の実装で並列化のパフォーマンスを上げるためには、 spliterator メソッドをオーバーライドしてチューニングする

Steram の仕様に厳密に従わなければ並列化した時の振る舞いが不安定になったりする。 forEach の代わりに forEachOrdered を使うと並列のストリームを遭遇順序 (encounter order) で走査することを保証できる

ストリーム中の要素数と1要素ごとに実行されるコードの行数の積が 10 万以上である時に並列化をするメリットがある。

並列化はあくまでも最適化であるため、最適化前と後で性能調査をする。また、実行は共有された fork-join プールを使うため、詰まると周りに迷惑をかける

乱数のストリームの並列化なら、ThreadLocalRandom ではなく、SplittableRandom を使う。

kawasin73 commented 4 years ago

8章 メソッド

item 49 : パラメータの正当性を検査する

パラメータの値の制約をメソッドの初期段階で確認するエラーを発生させる。発生させるエラーは、IllegalArgumentException, IndexOutOfBoundsException, NullPointerException が多い。

public と protected の場合はスローされる例外を Javadoc の @throws に明記する。

NullPointerException などの全てのメソッドで発生しうる例外についてはクラスレベルで宣言することで、メソッドごとに重複して宣言しなくてもいい。 @Nullable アノテーションは標準ではなく、複数のアノテーションが同じ目的で使われることがあるのでオススメされない

java.util.Objects の便利な検査メソッド

private メソッドの場合は引数に渡す値は管理されたものなので assert を使う。AssertionError を発生させる。java コマンドに -ea または -enableassertions をつけると有効になる。無効な場合のコストはない。

なるべく早いタイミングで値のバグを検出することは重要。特にコンストラクタでのチェックは重要。

正当性検査のコストが高い、現実的でない場合、正当性検査が処理の中で暗黙に行われる場合は、パラメータの検査をする必要はない。

例外翻訳 : item 73

そもそもパラメータに制約がない方がいい。

item 50 : 必要な場合、防御的にコピーする

クラスの不変式を破壊されないために防御的にプログラムする

不変でないオブジェクトをクラスの内部に注入された場合、外部から変更される可能性がある。 不変でないオブジェクトはそのままフィールドに設定するのではなく、防御的にコピーする。正当性の検査はコピーしたオブジェクトに対して行う。渡されたオブジェクトは別スレッドから一時的に変更される可能性がある。無防備な時間(window of vulnerability)。TOCTOU 攻撃

clone() メソッドも Date などの final でないクラスの場合は信用できない。

内部の値を直接外部に露出させるのではなく、防御的にコピーする。こちらでは clone を使ってもいい。clone が override されていないことを保証できるから。ただし、一般にはコンストラクタや static ファクトリを使うのが良い。

内部の値へのアクセスをさせないということは不変クラス以外でも重要。

不変オブジェクトを使えばこの辺りは気にしなくてもいい。

パフォーマンスが問題になることがある。パッケージ内部でのみ使う場合などは妥協する。また、クライアントが要素を変更しないと信頼できる時。

item 51 : メソッドのシグニチャを注意深く設計する。

item 52 : オーバーロードを注意して使う

オーバーロードは、同じメソッド名で引数の型が違うように定義すること

オーバーロードされたメソッドのうちどれが呼び出されるかはコンパイル時に決定する。

オーバーロードされたメソッドの選択は静的、オーバーライドされたメソッドの選択は動的

オーバーロードの対策としては、instanceof を使って実行時に型を検査する。

困惑させるようなオーバーロードの使用は避ける。メソッド名を変えて対応する

複数のコンストラクタがある場合全てオーバーロード。static ファクトリメソッドで対応できる。また、キャストすることで避けることもできる。

同じパラメータ数の場合は、明らかに異なる型同士にする。使うときは基本データ型の自動ボクシングに注意する。 例:List.remove(int)List.remove(Object)

関数型インターフェイスを受け取るオーバーロードはしない。ラムダが絡むとわかりにくくなる。

item 53 : 可変長引数を注意して使う

可変長引数は、渡された引数と同じ長さの配列を確保して詰める。引数は 0 個以上。 1つ以上の引数を受け取りたい場合は、コンパイル時のチェックが難しく、実行時エラーとして扱うことになる。 1つ以上の引数を受け取りたい場合は、独立の1つの引数として引数に定義してしまう。

static int min(int firstArg, int... remainingArgs);

メソッド呼び出しごとに配列を確保するためパフォーマンスが重要な場合には使えない。パフォーマンスが重要な場合は、0~3個までの引数に対応するオーバーロードしたメソッドを用意し、4つ以上については可変長引数で対応するようにする。 95%のメソッド呼び出しは3個以下の引数らしい。

item 54 : null ではなく、空コレクションか空配列を返す

クライアント側に null チェックを強制してしまい、複雑になってしまうのでよくない。また、null チェックを忘れた場合でも大抵の場合は要素があるのでバグに気付きにくい。

配列やリストオブジェクト確保のコストを気にするのは早すぎる最適化。また、パフォーマンスの課題になる場合は、同一の不変空コレクションを返すことで回避できる。Collections.emptyList()。ただし、不変空コレクションの利用は最適化であるため、パフォーマンスの計測が必要。

item 55 : オプショナルを注意して返す

値を返さないメソッドの方法

Java8 以降で Optional<T> が追加された。Optional.empty()Optional.of(value)ofnull を渡すと NullPointerException が発生。Optional.ofNullable(value) では null だと空オプショナルを返す。 Optional を返すメソッドで null を返してはならない。

ストリームの終端操作の多くはオプショナルを返す。

利点

java9 で Optional に stream() メソッドが追加されてストリームに変換できるようになった。

コレクション、ストリーム、マップ、配列、オプショナルを含むコンテナ型はオプショナルで包むべきではない。

Optional 生成分のコストはあるのでパフォーマンスがシビアな場合は使わない。

基本データ型をボクシングして Optional にするのはコストが高いので OptionalInt, OptionalLong, OptionalDouble が提供されている。

キー、値、あるいはコレクションや配列の要素としてオプショナルを使うことは大抵適切でない。 戻り値以外でオプショナルを使うことは少ない。

item 56 : 全ての公開 API 要素に対してドキュメントコメントを書く

Javadoc でコードにドキュメントコメントを埋め込むことでドキュメントを自動生成できる。

全ての公開されているクラス、インターフェース、コンストラクタ、メソッド、フィールドの宣言の前にドキュメントコメントを書かなければならない。シリアライズ可能なら、シリアライズ形式も。

public のクラスはデフォルトコンストラクタを使うべきではない。ドキュメントできないから。 公開されていない要素についてもドキュメントコメントを書くことが望ましい。

kawasin73 commented 4 years ago

9章 プログラミング一般

item 57 : ローカル変数のスコープを最小限にする

item 58 : 従来の for ループよりも for-each ループを選ぶ

for-each ループはイテレータを隠蔽する。Iterable インタフェースを実装したオブジェクトに適用できる。配列も、コレクションも同じように扱える。 for-each ループはコンパイル時には for ループと同じようになるためペナルティはない。

for-each ループが使えない状況

item 59 : ライブラリを知り、ライブラリを使う

特に重要なのは、java.lang, java.util, java.io とそのサブパッケージ

item 60 : 正確な答えが必要ならば、float と double を避ける

float と double は、主に科学計算と工学計算のために設計されている。正確とは限らないので、金銭計算には特に使うべきではない。 金銭計算には BigDecimal か int, long を使う。

BigDecimal は不便で遅い。パフォーマンスが重要なら、int か long で小数点の位置を自分で管理しながら実装する。 9桁までは int, 18 桁までは long, それ以上は BigDecimal

item 61 : ボクシングされた基本データよりも基本データ型を選ぶ

両者の違い

ボクシングされた基本データに対する == はオブジェクトの比較を行うので間違えやすい。明示的にアンボクシングすれば解決。

ボクシングされた基本データと基本データ型を一緒に使うと、大抵の場合はアンボクシングされる。null では NullPointerException が発生する可能性がある。

ボクシングされた基本データの使い道

item 62 : 他の型が適切な場所では、文字列を避ける

item 53 : 文字列結合のパフォーマンスに用心する

文字列の + は1行の生成や、小さな固定サイズの文字列の構築には向いている。 しかし、一般に n 個の文字列を結合するのに O(n^2) の時間を必要とする。文字列は不変であるのでコピーが毎回走るから。

StringBuilder を使う。

item 64 : インタフェースでオブジェクトを参照する

パラメータ、戻り値、変数、フィールドは可能な限りインタフェース型で宣言する。 具体的なクラスを参照するのはコンストラクタを呼ぶ時だけ。 それによって柔軟性が得られる。

同じインタフェースを使う場合でもインタフェースで定義されている以上の契約に依存する場合は注意が必要。

インタフェース型がない場合はそのクラスで宣言するのも良い。

item 65 : リフレクションよりもインタフェースを選ぶ

コアリフレクション機能 : java.lang.reflect

デメリット

代表的な利用用途:コード解析ツール、依存性注入ツール 大抵の場合はリフレクションは必要ない。

リフレクションでインスタンスの生成のみを行い、メソッドなどへのアクセスはインタフェースやスーパクラスを通して行う。

item 66 : ネイティブメソッドを注意して使う

JNI : Java Native Interface C や C++ のネイティブメソッドを呼ぶ

ネイティブメソッドの利用用途

デメリット

item 67 : 注意して最適化する

早すぎる最適化は悪。 速いプログラムよりも優れたプログラムを パフォーマンスを制限するような設計はしない。モジュール間や外部とのやり取りの API やプロトコル。

item 68 : 一般的に受け入れられている命名規則を守る

活字的命名規約 (typographical)

パッケージ名とモジュール名は、ピリオドで区切られた要素で階層的であるべき。逆順のドメイン。java, javax は例外。パッケージ名は 8 文字以下が好ましい

文法的命名規約 (grammatical)

kawasin73 commented 4 years ago

10章 例外

item 69 : 例外的状態にだけ例外を使う

制御フローとして例外を使わない。API を設計するときも、通常の制御フローに例外を使わない。検査メソッドを提供するべき。または、空のオプショナルか null を返して戻り値で区別する。 並行処理の干渉の可能性があるときは戻り値での区別を選択する。大抵の場合は検査メソッドを提供するのがいい。検査メソッドを忘れて呼び出した時に例外を発生させるとバグに気付きやすくなる

item 70 : 回復可能な状態にはチェックされる例外を、プログラミングエラーには実行時例外を使う

3 種類の例外

呼び出し元が適切に回復できるような状況に対してはチェックされる例外を使う。catch を強制できる。これは呼び出しの結果としてエラーが起こる可能性があることを表明することになる。

後者2つはチェックされない例外。振る舞いは同じ。一般にはキャッチするべきではない。

プログラミングエラーを表す時に、実行時例外を使う。事前条件違反。 プログラムエラーなのか、回復可能な状態を扱っているのかが必ずしも明らかではない。

エラーは、JVM のために予約されている。JVMの実行な不可能な場合、資源不足などで発生する。Error サブクラスは作らない。AssersionError 以外の Error をスローしない。

チェックされない例外は、RuntimeException をサブクラス化する。

追加の情報を付与するためにフィールドやメソッドを例外に実装する。文字列に含めない。

item 71 : チェックされる例外を不必要に使うのを避ける

チェックされる例外は使う側に負担。 ストリームではチェックされる例外は使えない。

チェックされる例外は、適切に API を使った時に防ぐことができず、かつ、ユーザーがそれに対して何らかの有用な処理を行うことができる場合にのみ利用する。

新しくチェックされる例外を追加することは大きな変化であり、避けたい。オプショナルを返すことで回避可能。ただし、詳細な情報を返すことができない。 例外がスローされるかを検査する boolean のメソッドを追加することで、チェックされる例外をチェックされない例外に変換する。ただし、並行処理での状態遷移の可能性に注意する

item 72 : 標準的な例外を使う

コードの再利用性は大切。

Exception, RuntimeException, Throwable, Error を直接使わない。抽象クラスのように扱う。

よく再利用される例外

例外 使う機会
IllegalArgumentException null ではないがパラメータ値が不適切
IllegalStateException メソッド呼び出しに対してオブジェクト状態が不正
NullPointerException パラメータ値が禁止されている null
IndexOutOfBoundsException インデックスパラメータ値が範囲外
ConcurrentModificationException 禁止されているオブジェクトの並行した変更を検出
UnsupportedOperationException オブジェクトがメソッドをサポートしていない

そのほかの既存の例外を再利用することも可。その例外のドキュメンテーションと矛盾しない、名前、セマンティックスに基づいている。 また、既存の例外をサブクラス化して拡張するのも可。ただし、例外はシリアライズ可能であることに注意。

IllegalArgumentExceptionIllegalStateException の区別は、どんな引数を渡してもうまく動作しないときは IllegalStateException

item 73 : 抽象概念に適した例外をスローする

下位のレイヤーからの例外をそのまま再利用するのではなく、そのレイヤーの抽象度に適した例外に変換してスローする。実装の詳細で汚染されてしまう。例外翻訳(exception translation)

下位の例外のコンテキストが必要な場合、例外連鎖を行う。上位の例外が Throwable をコンストラクタで引き受ける時に可能。 連鎖可能なコンストラクタを持たない場合は、Throwable の initCause を使う。

最善なのは、例外翻訳を乱用するのではなく、例外が発生しないように事前に検査をすること。または、上位レイヤで例外処理をして例外を発生させないこと。

item 74 : 各メソッドがスローする全ての例外を文書化する

@throws を使って Javadoc に各例外がスローされる条件を正確にドキュメント化する。スーパークラスをスローするような手抜きはダメ。main メソッドは例外で Exception をスローすると記述せざるを得ない。

チェックされない例外についてもドキュメント化することが賢明。事前条件を記述することにもなる。ただし、チェックされない例外については @throws に書いて、throws には追加しない (?)

インタフェースでもチェックされない例外を文書化することで一般契約の一部を表せる。

クラス内で共通の例外を発生させるときは、クラスのドキュメンテーションにかく。(NullPointerException) など

item 75 : 詳細メッセージをエラー記録情報に含める

スタックトレースには、例外の toString() の結果が含まれる。のちの分析のための情報(例外の原因となったすべてのパラメータとフィールドの値)をエラーの詳細情報に記録するべき。 セキュリティに関わることは含めない。パスワードや鍵など 不必要に長くしない。 エンドユーザへのエラーメッセージと混同しない。 パラメータを例外のコンストラクタで受け取ってメッセージを生成するのもあり。

item 76 : エラーアトミック性に努める

失敗したメソッド呼び出しはオブジェクトをそのメソッド呼び出しの前の状態になっているべき。(そのエラーが回復することを期待されているとき)

不変オブジェクトなら簡単。 可変オブジェクトなら

エラーは一般に回復不能。エラーアトミック性を頑張る必要はない。 エラーアトミック性が破られるようなときはドキュメンテーションする

item 77 : 例外を無視しない

空の catch ブロックはダメ。

無視することが適切な場合もある。回復が必要ない場合など。無視するときは、エラー変数名を ignored にして無視する理由などをコメントする

kawasin73 commented 4 years ago

11 章 並行性

item 78 : 共有された可変データへのアクセスを同期する

synchronized 予約語によって相互排他(mutual exclusion)ができる。

相互排他によって、オブジェクトの不整合な状態がほかのスレッドに見えることを防ぐ。また、変更が確実に他のスレッドにも見えることを保証する。

Java では、long, double 以外の変数の読み書きは atomic である。読み出しで値が壊れることはないが、どのスレッドによって変更された値が読み出せるかはわからない。

メモリモデル : あるスレッドによる変更が他のスレッドからいつ、どのように見えるかを定義

Thread.stop は安全ではないから使ってはならない。boolean の値フィールドがアトミックに読み書きできるから、その値をポーリングして停止するかを判断する。ただし、synchronized を使わないと値の変更の伝搬が保証されないし、コンパイルの最適化でうまくいかないこともある。(巻き上げ hoisting) 書き込みも読み込みも synchronized で同期する。

ただし、読み書きが atomic であるときは synchronized を通信効果のためだけに使うのは大げさ。volatile を使うことで相互排他はしないが、値の読み込みで最後に書き込まれた値が見えることを保証する。

ただし、nextSerialNumber++ は読み込みと書き込みを行うので排他制御が必要 (synchronized を使う)。 java.util.concurrent.atomic の AtomicLong を使うとvolatile の通信効果とアトミック性を lock-free で提供するので良い。

可変データを共有しないことでこれらの問題を回避できる。可変データを単一スレッドに閉じ込める。そのときは使い方をドキュメントに明示する。

再度オブジェクトを変更しない場合は、事実上不変(effectively immutable)として共有できる。

item 79 : 過剰な同期は避ける

過剰な同期は、パフォーマンス低下、デッドロック、予想外の振る舞いを引き起こす可能性がある

活性エラー(通信効果)と安全性エラー(データ不整合)を避けるために、同期されたメソッドやブロック内で制御をクライアントに譲らない。オーバーライドされるように設計されたメソッドや関数オブジェクトを同期された領域内で呼び出さない。それらのメソッドは異質(alien)である。

Java の synchronized は再入可能(reentrant)。同一スレッド内でネストしてロックを獲得することができる。ただし、別スレッドで synchronized を待ち合わせるとデッドロック。 再入可能だが、活性エラーを安全性エラーに変える可能性がある。

異質な呼び出しを同期ブロックの外で行うことで解決する(オープンコール)。コピーを取るときは、コンカレントコレクションが提供する CopyOnWriteArrayList が便利。通常はパフォーマンスが悪いが、滅多に変更されずに走査されるだけの時には有用。

オープンコールは、デッドロックを避けるだけでなく、不必要に長いロックを防ぐ。

同期された領域内での処理は最小限にする。同期のコストは、ロックの獲得ではなく競合の方が大きい。

可変クラスを並行に使えるようにする方法

基本は前者。後者に明確なメリットがない場合は、クラスは同期しないでスレッドセーフでないことをドキュメント化する。 内部的に同期するときのヒント:ロック分割、ロックストライピング、非ブロッキング並行性制御

static なフィールドはグローバルである。

item 80 : スレッドよりもエグゼキュータ、タスク、ストリームを選ぶ

エグゼキュータサービスは便利。特定のタスクの待ち合わせ、すべてのタスクやあるタスクグループの待ち合わせなど様々なことができる。 スレッドプールから、複数のエグゼキュータサービスを生成することもできる。

Thread は処理の単位と実行する機構の2つの役割 エグゼキュータサービスでは分離されている。処理の単位はタスクと呼ばれる。Runnable と Callable。Callable の方は値を返せるし例外をスローできる。 実行する機構がエグゼキュータサービス

fork-join はタスクの steal をする

item 81 : wait と notify よりも並行処理ユーティリティを選ぶ

java.util.concurrent の高レベルのユーティリティ3つ

wait と notify を使うとき

wait と notify は同期された領域内で使う必要がある。条件変数的な使い方

wait は条件を検査する while ループの中(wait ループイディオム)で使う。ループの外で呼び出してはいけない。活性を保証するために待ちの前に検査することは必要。安全性を保証するために待ちの後に検査することは必要。

item 82 : スレッド安全性を文書化する

synchronized が使われているからといってスレッド安全とは限らない。

スレッド安全性のレベル

一般にはクラスにドキュメントするが、特別なスレッド安全性特性を持つメソッドはメソッドコメントにドキュメントする。

enum の不変性をドキュメント化する必要はない。

Collections.synchronizedMap のように static ファクトリメソッドは返されるオブジェクトのスレッド安全性をドキュメント化する

誰もがアクセス可能なロックを使って長期間ロックを確保すると DoS に弱い。回避するためにプライベートロックオブジェクトを使う。final で宣言する。

private final Object lock = new Object();
public void foo() {
  synchronized(lock) {
    // do something
  }
}

ただし、条件付きスレッドセーフではドキュメント化する必要があるから使えない。

item 83 : 遅延初期化を注意して使う

lazy initialization 必要になるまで初期化を遅らせる。static フィールド、インスタンスフィールドの両方に適用可能。主な目的は最適化だが、初期化での循環を解決するための場合もある。

最適化であるので、パフォーマンス計測をする。

複数スレッドで使う場合は、スレッドセーフになるようにしないといけない。

循環を断ち切るためには、アクセサメソッドを synchronized にする。

static フィールドの遅延初期化は、遅延初期化ホルダー・クラス・イディオムを使う。クラスが使われるまでクラスが初期化されないことを利用している。

private static class FieldHolder {
  static final FieldType field = computeFieldValue();
}

private static FieldType getField() {
  return FieldHolder.field;
}

この時、アクセサは synchronized されないでいい。

インスタンスフィールドでは、二重チェックイディオムを使う。1回目はロックせずに検査し、初期化されていなかったらロックを取った上で再度検査する。ただし、volatile をつけることを忘れない。

private volatile FieldType field;

private FieldType getField() {
  FieldType result = field;
  if (result != null)
    return result;

  synchronized(this) {
    if (field == null)
      field = computeFieldValue();
    return field;
  }
}

result にコピーすることで field が1度しか読み込まれないようにする。これでパフォーマンスが向上することがある。 派生形に synchronized を使わない単一チェックイディオムもある。

再計算されても良くて double, long 以外であれば volatile を取っても良い。きわどい単一チェックイディオムと呼ばれる。

item 84 : スレッドスケジューラに依存しない

プラットフォームによってスレッドスケジューラのポリシーは異なる可能性がある。移植性を保つために依存しない。

頑強で応答性がよく、移植可能なプログラムのために、実行可能なスレッドの平均数をプロセッサの数よりもはるかに大きくしない。スレッドスケジューラのできることがすくなくなる。 有益な処理をしないときは、待たせる。ビジーウェイトをしない。

スレッドの CPU 時間が短いことの解決に Thread.yield を(スレッド実行権の放棄)使わない。 スレッドの優先順位の調整は移植性がなくなるのでしない。

kawasin73 commented 4 years ago

12 章 シリアライズ

item 85 : Java のシリアライズよりも代替手段を選ぶ

シリアライズのデメリット:見えないコンストラクタ、APIと実装間の曖昧な境界、セキュリティやパフォーマンス、正しさの問題

根本的な問題:攻撃対象領域が広すぎて保護できない。

シリアライズ可能な型をガジェットと呼ぶ。 ディシリアライズに時間がかかるものを送りつける DoS 攻撃。

信頼できないバイトストリームをディシリアライズしない。クロスプラットフォーム構造化データ表現を使う。JSONや protobuf

どうしても避けられないときは、ディシリアライズフィルターを使う。 java.io.ObjectInputFilter。ブラックリストよりもホワイトリストを使う。

item 86 : Serializable を最新の注意を払って実装する

implements Serializable を追加するだけでシリアライズ可能になる。ただし、一度リリースされると互換性のために大変になる。 private なフィールドも含まれるために公開 API の一部になってしまう。

コスト

気をつけること

サブクラスがシリアライズ可能であるためには継承元がシリアライズ可能であるか、パラメータなしのコンストラクタが必要。

内部クラスは Serializable にしない

item 87 : カスタムシリアライズ形式の使用を検討する

適切かどうかを最初に検討せずに、デフォルトのシリアライズ形式を受け入れてはいけない。

デフォルトのシリアライズ形式は、そのオブジェクト内に含まれるデータとそれに紐ずく到達可能なすべてのオブジェクトに含まれるデータを記述する。

オブジェクトの物理表現と論理的内容が同じ場合、デフォルトのシリアライズ形式はおそらく適切。

デフォルトのシリアライズ形式が適切であっても、不変式とセキュリティを保証するために多くの場合 readObject メソッドを提供しなければならない

デフォルトのシリアライズ形式を使うことのデメリット

効率的な writeObject と readObject を提供する。 同期にも気をつける 明示的なシリアルバージョンUIDを宣言する

private static final long serialVersionUID = xxxxxxx;

食い違うと InvalidClassException が発生する

item 88 : 防御的に readObject メソッドを書く

readObject メソッドは実質的にもう1つの public のコンストラクタ。 防御的コピーを使っている不変クラスでは、readObject でも引数の妥当性と防御的コピーをする必要がある。

不正なバイト列を防ぐために、readObject で不変式が満たされているか正当性の検査をする。不正な場合は、 InvalidObjectException また、防御的コピーもする

クラス内のオーバーライド可能なメソッドを呼び出してはいけない。

item 89 : インスタンス制御に対しては、 readResolve よりも enum 型を選ぶ

readResolve で単一インスタンスのみを再利用することを強制できる。無視されたインスタンスは GC される。

シングルトンのインスタンスの転送ではフィールドの値は必要ないから、transient と宣言する。然もなくば脆弱性

enum によって安全になる。

item 90 : シリアライズされたインスタンスの代わりに、シリアライズ・プロキシを検討する

Serializable を実装することはバグとセキュリティ問題の可能性を増大させる。 コンストラクタ以外でインスタンス化されるから。

シリアライズ可能なクラスの private static のネストしたクラス(プロキシ)を導入。コンストラクタでは、一貫性検査や防御的コピーは必要ない。 エンクロージングクラスに writeReplaace メソッドを追加して、プロキシを挟む。readObject は不変式の破壊を防ぐために潰す。 プロキシクラスの readResolve でエンクロージングクラスに変換する。このときはエンクロージングクラスの public なインタフェースでインスタンス化する。

ユーザによって拡張可能なクラスとは互換性がない。循環を含むようなクラスとも互換性がない場合がある。

プロキシを使うことでパフォーマンスが犠牲になることがある。