Closed kawasin73 closed 3 years ago
static factory method 自分自身を返り値にする public な static method
メリット
interface に static method を定義できるようになったのはJava 8 から。それ以前はインスタンス化不可能なクラスに static メソッドをつけていた
デメリット
Go の sql データベースのフレームワークとか
Java 6 以降は、java.util.ServiceLoader
がサービスプロバイダフレームワークとして使える
パラメータが多いと、コンストラクタも static ファクトリメソッドも使いづらい
freeze()
されるまで動かさないなどの工夫があるデメリット
シングルトンにするとテストが大変。
実現方法
public static final
のクラスフィールドに初期化private static final
のクラスフィールドに初期化private なコンストラクタであっても AccessibleObject.setAccessible
メソッドを使ってリクフレクションにより呼び出すことができてしまう。防ぐためにはコンストラクタでチェックして例外を throw する。
Serializable
にするためには注意が必要。
transient
を宣言するreadResolve()
メソッドを提供するenum を使ってシングルトンを提供すると便利。Serializable やリフレクションの問題は存在しない。ただし、あるクラスを継承したシングルトンは enum にはできない。
static なフィールドとメソッドのみを提供したいとき。 関数をグルーピングしたいときなどに使える
インスタンス化することは想定していないことを明示する
public class UtilityClass {
private UtilityClass() {
throw new AssertionError();
}
}
コメントをつけないと混乱する可能性がある
https://qiita.com/ryo2132/items/eb9a63f2b107c1d6b25c
静的なユーティリティクラスとシングルトンは、下層の資源でパラメータ化された振る舞いを持つクラスに対しては不適です。
コンストラクタで依存先を注入する。
メリット
依存資源としてファクトリオブジェクトを渡す応用もある。Java にはファクトリとして Supplier<T>
インターフェースがある。
immutable であると再利用しやすい。
文字列の初期化で new String()
で生成するとオブジェクト生成コストがかかる。単に ”hello”
と生成すると同じオブジェクトを使い回す。
static factory method と相性がいい。 初期化コストが高いものはキャッシュして使い回す方がいい
lazy initialization はそれほど効果がないらしい?
自動ボクシング (autoboxing) : primitive がボクシングされたデータ型に変換されてしまうこと
long
は Long
がボクシングしている
Long
に long
を足そうとすると、long
が Long
に変換されてオブジェクト生成が発生する。
ただし、JVM でのオブジェクト生成のコストは小さいものと考えたほうがいい。初期化処理が小さいオブジェクトは、自分でオブジェクトプールを管理するより JVM に任せたほうがいい。
item 50 では防御的コピーを取り上げる。これはこことは真逆。
obsolete reference : 使われなくなった参照
連鎖的に参照が残ってしまって大きなメモリリークに繋がることも。
解決策:使い終わったら明示的に null
を代入して参照を外す。
メモリ管理が間違っていたときに NullPointerException で早期に発見できる。
通常は null
を設定するのではなく変数をスコープ外に出すだけで十分。逆に null
を代入して回るのは複雑になりがち。
原因
WeakHashMap
が有効。LinkedHashMap.removeEldestEntry()
)Heap profiler などを使って見つけることになる。
finalizer は予想不可能であり危険。
クリーナーは Java 9 から導入された finalizer の代わり。クリーナーは独自のクリーナースレッドを指定できる。
メモリ以外の資源の回収のためには、try-with-resources
や try-finally
を使う。
実行タイミングは予期不能。
実行は保証されていない。
キャッチされない例外は握り潰される
実行には深刻なパフォーマンスのペナルティがある
ファイナライザ攻撃 : セキュリティ問題
ファイルなどの資源の終了処理は、 AutoCloseable
をクラスに実装する。ただし、2重 close を防ぐ( IllegalStateException
)などの対応は必要。
これを try-with-resources
ブロックから使ってもらう。
close メソッドの呼び忘れに対するセーフティネット。ただしコスト的に見合うかを検討する必要はある。
ネイティブピア(Java が管理しないネイティブのオブジェクト)の開放。重要性が高くない場合は finalizer を使うのもあり。
クリーナーを使うときは、循環参照を避けるために Runner を static なクラスにする必要がある。
try-finally
はうまくいかない。2つ目のリソースがあるとネストしてしまい見通しが悪い。
Java 7 で try-with-resources
文が導入された。 AutoCloseable
インターフェイスを実装すると対応できる。
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dest)) {
// ...
}
複数のリソースの確保にも対応している。 ログの隠蔽問題についてもスタックとレースに隠蔽された表記が追加されるため、わかりやすくなる。
Object のオーバーライドされる前提のメソッド
オーバーライドしないときはインスタンス自身とのみ equals は true になる
オーバーライドする必要のない場合
equals は論理的等価性の概念を持っているときに実装する。一般には値クラス
equals メソッドが実装する同値関係
x.equals(x) = true
x.equals(y) = true <=> y.equals(x) = true
それ以外は false
x.equals(y) = true, y.equals(z) = true => x.equals(z) = true
x.equal(y)
の結果は何度呼び出しても一貫している
equals(null)
に false
を返す
false
を返すinstanceof
で弾いた方が簡潔インスタンス化可能なクラスを拡張して equals の契約を守ったまま値要素を追加する方法はない
つまり、equals を一回 override したクラスを継承してもう一度 override はできないということ。
回避策としては継承を使わずにコンポジションを使う。
java.sql.Timestamp
は java.util.Date
を拡張しているがここでの対称性を守れていないので混ぜると危険。
==
によって検査すると、高コストな比較処理を省くことができるinstanceof
を使う==
Float.compare(float, float)
, Double.compare(double, double)
対称的、推移的、整合的 の3つを保証する単体テストを書く
注意事項
equals
を override するときは hashCode
をオーバーライドするequals
の引数は Object
のまま大抵は、Google の AutoValue
を使えば自動生成できる。
HashMap や HashSet でうまく動かなくなる
hashCode の一般契約
ハッシュ化の方法はいくつかある。31 を掛ける方法など。(31 の掛け算は JVM では最適化されてたりする)
衝突の少ないハッシュ関数は、com.google.common.hash.Hashing
を参照。
Objects.hash()
関数はパフォーマンスは良くないが簡単に利用できる。
計算コストが高い場合はキャッシュ化も考える
hashCode の内部実装に依存させないために仕様を定義しない。それによって利用側に柔軟性を強制できる
toString
には全ての興味のある情報を含めるべき。
toString
の結果の形式を定義する場合は、文字列からインスタンスを生成する static ファクトリメソッドを作ると便利。一方で定義すると変更できなくなってしまう。
形式を定義する場合でもしない場合でもその旨をコメントに明記する。
item 4 の static ユーティリティクラスには不要。enum もデフォルトのものが優秀なので新たに定義する必要はない。
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 を使うべきであるが、それ以外では推奨されない。
Comparable インターフェイスには、compareTo
がある。順序を表し、ソートなどの既存のアルゴリズムに簡単に統合できるようになる。
一般契約
ClassCastException
をスロークラスをまたがって動作しないので equals に比べれば楽。equals と同じように継承して override するとうまく動かない。
Float や Double などの基本型では、 <, > ではなく compare を使う
コンパレータ構築メソッド(comparator construction method)を使うと簡潔に実装できる。
引き算で compareTo を実装することは整数のオーバーフローや浮動小数点数算術の副作用の危険があるため推奨されない。Integer.compare か、Comparator を使う。
情報の隠蔽、カプセル化を実現する。 外部に公開することが少ないと、内部の実装を変更しても外部への影響が小さくなる(最適化が容易になる)。
トップレベルのクラス、インターフェイスでは、public
をつけた時のみパブリックになる。それ以外はパッケージプライベート。
パッケージプライベートなクラスが1つのクラスからのみ利用されている場合は、ネストしたクラスにすることで可視性を狭められる。
基本的に全て private にして、うまくいかない時に適切なレベルを選ぶ。 Serializable を実装するとプライベートなフィールドが公開 API の中に漏れてしまう。
サブクラスのメソッドはスーパークラスの可視性を狭めてはいけない。コンパイルエラーになる。
テスト容易性のために private をパッケージプライベートにしてもいいが、それ以上の公開は許されない。
インスタンスフィールドは public にするべきではない。外部から変更されてスレッドセーフではなくなる。また型の変更ができなくなる
static フィールドも同様に public にするべきではないが、大文字+スネークケースで構成される定数は例外
配列の中身は可変であることに注意。基本的に public にするべきではない。
Java 9 からはモジュールシステムが追加されている。モジュールはパッケージをグループ化する仕組み。モジュール内の公開されていない public と protected はモジュール外からはアクセスできない。
final であれば害は少ない。
Immutable なクラスを提供するための5つのクラス
操作の結果は関数の返り値として新しいオブジェクトが生成されて返ってくる。関数的な方法。 手続き的な方法ではない。関数名が動詞ではなく、前置詞である。
可変クラスは、状態遷移をするため管理が複雑になる。不変クラスはスレッドセーフになる。不変で共有できるため、キャッシュもできる。
コピーしても全く同じものがコピーされるだけなので意味がない。防御的コピー、clone メソッド、コピーコンストラクタは必要ない。
不変クラスの実装で内部表現を共有することもできる。
欠点は、個々の異なる値に対して別のオブジェクトを生成してしまうこと。コストの大きな変換処理ではステップごとにオブジェクト生成してしまう。 解決策1:複数ステップを1ステップにまとめた変換メソッドを提供 解決策2:public の可変コンパニオンクラス(StringBuilder など)
パフォーマンスのために、externally visible な変更をしないという制約のもとで上の5つの規則を緩めることができる。(内部での一貫性のあるキャッシュなど)
基本的にフィールドは、private final にすることが望ましい。
パッケージをまたがって、具象クラスから継承することは危険。メソッド呼び出しとは異なり、継承はカプセル化を破る。サブクラスはスーパークラスの実装に依存するため、スーパークラスの実装の変更に弱い。新しいメソッドがスーパークラスに追加された時に実装もれが発生したりする。
コンポジションにして、メソッド呼び出しを forwarding する。
forwarding クラスを実装すると使いまわせる。
用語:wrapper, decorator pattern, delegation
ラッパークラスの欠点:callback framework には向いていない。子クラスはラッパーの存在を知らないので自分自身を登録してラッパーを回避してしまう。(SELF 問題)
メソッド呼び出しのオーバーヘッドやラッパーのオーバーヘッドは大きな問題にはならない。
subclass は subtype である。is-a
関係が成り立っていない時はコンポジションを使う。Java では Stack-Vector や Properties-Hashtable は間違っている。
継承によってスーパークラスの API を引き継ぐことになる。APIが欠陥を持っているときにその欠陥が伝播させられる。
@implSpec
に実装要件を記述する。
Java 8 で default メソッドが導入され、abstract クラスと interface は同じ機能を持つ。ただし、Java は単一継承なので抽象クラスは使いづらい。
interface の制約を回避するために、抽象骨格実装(skeletal implementation) クラスは interface と abstract クラスの長所を組み合わせる。Template Method パターン。
命名の慣習として、Abstract<Interface_name>
がよく使われる。
Java8 より前は default メソッドがなかったので interface へのメソッドの追加は即コンパイルエラー。デフォルトメソッドでメソッドの追加は可能になったが、全ての継承された先において安全な実装であるとは言えない。
デフォルトメソッドによって interface へのメソッドの追加は可能であるが、避けるべきで、最初に慎重に設計する方が大事。
定数インターフェイス(メソッドのないインターフェイス)はアンチパターン。インターフェイスの趣旨に反しているから
java.io.ObjectStreamConstants
は例外
定数は、クラスや enum、ユーティリティクラスなどで定数は提供するべき。
数値の中の _
は無視されるから見やすくするために使うのが良い。
タグ付きクラスは、内部に type
などの具象を表すフラグを持っておき、switch
文などで動作を分岐するようなクラス。
サブタイプを使うことでわかりやすく、効率的になる。
nested class は4種類
Java の仕様としては可能だが、重複定義された時の挙動は未定義。 わざわざややこしいことはしない!
Java 5 以降でジェネリクスが使える ジェネリクスがキャストよりもいい点は、エラーが実行時ではなくコンパイル時に発生すること
1つ以上の型パラメータを持つクラスやインターフェイスを、ジェネリッククラス、ジェネリックインターフェイスと呼ぶ。まとめてジェネリック型。
List<E> -> List<String>
では
E
String
List
要素型がわからないような場合は、原型ではなく、unbounded wildcard type (非境界ワイルドカード型) を使う。
List<?>
Collection<?>
には null 以外のオブジェクトを入れることができない。read only? によって型を守っている?
クラスリテラルでは原型を使わないといけない。クラスリテラル (List.class
) にパラメータ型は使えない。
instanceof
でも原型を使うことが望ましい。ただし型検査をした後は、Set<?>
などの非境界ワイルドカード型にキャストして使う。
頑張って無検査警告を解決していこう。
diamond operator (ダイアモンド演算子) <>
で型推論がされる。
確実に安全で警告を取り除けないときは、 @SuppressWarnings("unchecked")
アノテーションをつける。ただし最小のスコープで。return 文は宣言ではないのでつけられないから、ローカル変数を宣言してつける。
コメントをつけることも重要。
Sub[]
が Super[]
のサブクラス
ジェネリック配列の生成はコンパイルエラーになる。可変長引数では配列が生成されるので注意が必要。解決策もある。
配列よりもコレクション型の List
配列とコレクション型を一緒に使おうとするとコンパイルエラーや警告が発生する。その時は配列をリストに変換すると良い。(パフォーマンスは若干劣化するが安全になる)
Objectを扱いキャスト前提であるクラスを後から互換性を保ったままジェネリック型に変換できる。
Object 型をパラメータ型に置き換える。エラーに対処していく。
E[]
の生成でエラーになるときは、Object[]
を生成して E[]
にキャストするか、Object[]
で保持して利用時に E
にキャストするか。
ジェネリック型の中で private であれば配列を使うのもあり。
型パラメータに基本型(int, float など)を使うことはできず、ボクシングした型を使う
メソッドでの型パラメータの宣言は、メソッドの修飾子と戻り値型の間に。
ジェネリックシングルトンファクトリ : 恒等関数の生成などで使い回しを表現するときに便利
recursive type bound (再帰型境界) : <E extends Comparable<E>>
パラメータ化された型は不変である。時々これが不自由になる時がある。
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 では特に有効
ジェネリックスと可変長引数は Java 5 で同時に追加されたが協調しない。
本来はジェネリックの可変長引数はコンパイルエラーにするべきだが、利便性が高いため Java はこの不整合を受け入れている。
@SafeVarargs
アノテーションをメソッドにつけることで型安全であることを明示する。パラメータ化された型の可変長引数を持つメソッドでは必ずつけるようにする。
static か final か private でのみ使える。オーバーライドされないために
安全であるためには以下が必要
ジェネリクスのパラメータ配列はコンパイル時は Object[]
に割り当てられるため、バグの温床になる。
代替手段として、可変長パラメータではなくリストを受け取るようにする。
ジェネリクスの主な用途 : 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
でキャストできる。
列挙型 (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 文を使う。
enum の実体は int 値と関連づいており、ordinal()
メソッドで取得できるが、これに依存すると保守が大変。代わりにインスタンスフィールドに定義することで保守が楽になる。
ビットフィールドは、ビット和操作によって集合を表せるので便利だが、int enum 以上のデメリットがある。ビット幅を固定するので変更ができない、printable でない。
java.util.EnumSet が解決する。Set インタフェースを実装している。内部ではビットベクトルを保持している。
java 9 の時点では immutable な EnumSet に対応していないのが弱点
配列のインデックスに ordinal()
の値を使うのは良くない。
配列はジェネリックスとの相性が悪い。数値はラベルを手動でつけないといけない。誤った int 値の利用は実装者の責任。
java.util.EnumMap では enum をキーに使える。EnumMap は内部的には配列を使っているので十分速い。使うときはコンストラクタに Class オブジェクトを渡す必要がある。
ストリーミングでは、groupingBy に EnumMap を組み合わせることで最適化できる。
enum 型を外部から直接拡張するすることはできないし、継承して拡張することもできない。
共通した interface を実装した enum を用いることで同じ API で使えるため enum を拡張できるようになる。
enum の継承はできないのでメソッドの共有はできないが、interface のデフォルトメソッドとして定義することで重複を防ぐことができる。
命名パターンの欠点
アノテーションで解決する。JUnit はリリース 4 から採用。
メタアノテーション:アノテーションに対するアノテーション
パラメータなしの static のメソッドに対してのみ付与することは強制できないためコメントに書いている。強制するためにはアノテーションプロセッサを書く必要がある。(javax.annotation.processing)
パラメータを持たないアノテーションは、マーカーアノテーション。
リフレクションができる。Method.invoke()
でメソッドを実行。実行中のエラーは InvocationTargetException に Wrap されて送出される。
アノテーションインタフェースに value()
を指定することでパラメータを受け取れる。
コンパイル時にはアノテーションパラメータは正しかったけど、実行時に例外型を表すクラスファイルがなかった場合、TypeNotPresentException
が発生する。
アノテーションパラメータは配列にすることで複数受け取れる。設定する時は単一要素を指定することもできるし、{}
でカンマ区切りを囲って複数指定もできる。
@Repeatable
にコンテナアノテーション型を指定することで同じアノテーションを同時に複数設定できるようになる。
getAnnotationsByType()
では正しく振舞うが、isAnnotationPresent()
は単一のアノテーション型をコンテナアノテーション型を区別し、コンテナアノテーション型であるかどうかをチェックするので正しく振る舞わない。
@Override
をつけると、オーバーライドではなくオーバーロードしてしまった バグにコンパイラが気づく。
オーバーライドするメソッドが抽象クラスの抽象メソッドの場合は Override をつける必要はない。
インターフェイスのメソッドの実装に @Override
をつけるのも良い。
マーカーインタフェース : メソッドを持たない interface
マーカーインタフェースのマーカーアノテーションにない長所
ObjectOutputStream.write
メソッドはマーカーアノテーションの長所
Java 8 で関数型インタフェース、ラムダ、メソッド参照が追加された
単一の抽象メソッドを持つインターフェイスを無名クラスとして使って関数オブジェクトを表していた。これを関数型インタフェースと呼ぶ。 無名クラスはラムダ式で置き換えることができる。
ラムダでは型は省略できる。型を明示することでプログラムが明瞭になるわけでなければ型は省略するのが良い。コンパイラが型推論できなかった時に型をつける。
enum の定数固有クラス本体でのメソッド定義よりも、ラムダを使った enum インスタンスフィールドの方が簡潔に表せる。ただし、式の中身が簡潔な場合。また、enum のコンストラクタからは enum のインスタンスフィールドにアクセスできない。
ラムダでは1行が理想。長くても3行。
ラムダは抽象クラス、複数のメソッドを持つインタフェースには対応していない。無名クラスが対応している。ラムダでの this
はエンクロージングインスタンスを表す。
ラムダはシリアライズを確実に行えない。
メソッド参照はラムダよりも簡潔な関数オブジェクト生成方法。 ラムダにできなくてメソッド参照にできることはない。
メソッド参照の種類
メソッド参照の種類 | 例 | 同等のラムダ |
---|---|---|
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] |
ラムダなどの関数オブジェクトを受け入れるために独自のインタフェースを定義するのではなく、標準の 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
アノテーションによって関数型インタフェースであることを明示できる
関数型インタフェースを使ったメソッドを定義するときは、同じ引数の位置に関数型インタフェースを定義するメソッドを複数定義すると使う側が不便。
ストリームパイプラインには中間操作と終端操作がある。
ストリームパイプラインは遅延して評価される(lazily)。評価は終端操作が呼び出されるまで開始されないし、終端操作を完了させるために必要のないデータ要素は計算されない。
ストリームパイプラインは parallel
メソッドを呼び出すと並列実行される。
コードブロック(ループ)にできてラムダ(関数オブジェクト)にできないこと
ストリームが得意なこと
ストリームでは値を別の値にマッピングすると古い値は失われるので複数のステージの値を使う処理には向いていない。元の値を復元できる場合は対処できる。
ストリーム要素の変数名は複数名詞が望ましい。
ぶっちゃけ、ループを使うかストリームを使うかは好み
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引数にはマップファクトリを指定できる。
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
を利用してコレクションを実装することも検討する。
コレクションが無理なら自然な方を返す。
CPU 使用率が跳ね上がって高止まりして処理は進まない : 活性エラー (liveness failure)
パイプラインの並列化がパフォーマンスの向上につながらない場合
並列で実行する場合は余分に要素を処理して必要のない結果は捨てることで limit と共存する
見境なくストリームパイプラインを並列化しない。
並列化によるパフォーマンスの向上が得られるのはサブレンジへの分割が低いコストでできる以下の要素
また、これらの要素は順次処理で参照の局所性がある
終端処理が重い、順次行われる場合は並列化の恩恵は限定的。
独自の Stream, Iterable, Collection の実装で並列化のパフォーマンスを上げるためには、 spliterator メソッドをオーバーライドしてチューニングする
Steram の仕様に厳密に従わなければ並列化した時の振る舞いが不安定になったりする。 forEach の代わりに forEachOrdered を使うと並列のストリームを遭遇順序 (encounter order) で走査することを保証できる
ストリーム中の要素数と1要素ごとに実行されるコードの行数の積が 10 万以上である時に並列化をするメリットがある。
並列化はあくまでも最適化であるため、最適化前と後で性能調査をする。また、実行は共有された fork-join プールを使うため、詰まると周りに迷惑をかける
乱数のストリームの並列化なら、ThreadLocalRandom ではなく、SplittableRandom を使う。
パラメータの値の制約をメソッドの初期段階で確認するエラーを発生させる。発生させるエラーは、IllegalArgumentException
, IndexOutOfBoundsException
, NullPointerException
が多い。
public と protected の場合はスローされる例外を Javadoc の @throws
に明記する。
NullPointerException などの全てのメソッドで発生しうる例外についてはクラスレベルで宣言することで、メソッドごとに重複して宣言しなくてもいい。
@Nullable
アノテーションは標準ではなく、複数のアノテーションが同じ目的で使われることがあるのでオススメされない
java.util.Objects
の便利な検査メソッド
Objects.requireNonNull
で簡単に null 検査ができるcheckFromIndexSize
, checkFromToIndex
, checkIndex
private メソッドの場合は引数に渡す値は管理されたものなので assert を使う。AssertionError
を発生させる。java コマンドに -ea
または -enableassertions
をつけると有効になる。無効な場合のコストはない。
なるべく早いタイミングで値のバグを検出することは重要。特にコンストラクタでのチェックは重要。
正当性検査のコストが高い、現実的でない場合、正当性検査が処理の中で暗黙に行われる場合は、パラメータの検査をする必要はない。
例外翻訳 : item 73
そもそもパラメータに制約がない方がいい。
クラスの不変式を破壊されないために防御的にプログラムする
不変でないオブジェクトをクラスの内部に注入された場合、外部から変更される可能性がある。 不変でないオブジェクトはそのままフィールドに設定するのではなく、防御的にコピーする。正当性の検査はコピーしたオブジェクトに対して行う。渡されたオブジェクトは別スレッドから一時的に変更される可能性がある。無防備な時間(window of vulnerability)。TOCTOU 攻撃
clone()
メソッドも Date
などの final でないクラスの場合は信用できない。
内部の値を直接外部に露出させるのではなく、防御的にコピーする。こちらでは clone
を使ってもいい。clone
が override されていないことを保証できるから。ただし、一般にはコンストラクタや static ファクトリを使うのが良い。
内部の値へのアクセスをさせないということは不変クラス以外でも重要。
不変オブジェクトを使えばこの辺りは気にしなくてもいい。
パフォーマンスが問題になることがある。パッケージ内部でのみ使う場合などは妥協する。また、クライアントが要素を変更しないと信頼できる時。
オーバーロードは、同じメソッド名で引数の型が違うように定義すること
オーバーロードされたメソッドのうちどれが呼び出されるかはコンパイル時に決定する。
オーバーロードされたメソッドの選択は静的、オーバーライドされたメソッドの選択は動的
オーバーロードの対策としては、instanceof
を使って実行時に型を検査する。
困惑させるようなオーバーロードの使用は避ける。メソッド名を変えて対応する
writeInt
writeDouble
など複数のコンストラクタがある場合全てオーバーロード。static ファクトリメソッドで対応できる。また、キャストすることで避けることもできる。
同じパラメータ数の場合は、明らかに異なる型同士にする。使うときは基本データ型の自動ボクシングに注意する。
例:List.remove(int)
と List.remove(Object)
関数型インターフェイスを受け取るオーバーロードはしない。ラムダが絡むとわかりにくくなる。
可変長引数は、渡された引数と同じ長さの配列を確保して詰める。引数は 0 個以上。 1つ以上の引数を受け取りたい場合は、コンパイル時のチェックが難しく、実行時エラーとして扱うことになる。 1つ以上の引数を受け取りたい場合は、独立の1つの引数として引数に定義してしまう。
static int min(int firstArg, int... remainingArgs);
メソッド呼び出しごとに配列を確保するためパフォーマンスが重要な場合には使えない。パフォーマンスが重要な場合は、0~3個までの引数に対応するオーバーロードしたメソッドを用意し、4つ以上については可変長引数で対応するようにする。 95%のメソッド呼び出しは3個以下の引数らしい。
クライアント側に null チェックを強制してしまい、複雑になってしまうのでよくない。また、null チェックを忘れた場合でも大抵の場合は要素があるのでバグに気付きにくい。
配列やリストオブジェクト確保のコストを気にするのは早すぎる最適化。また、パフォーマンスの課題になる場合は、同一の不変空コレクションを返すことで回避できる。Collections.emptyList()
。ただし、不変空コレクションの利用は最適化であるため、パフォーマンスの計測が必要。
値を返さないメソッドの方法
Java8 以降で Optional<T>
が追加された。Optional.empty()
と Optional.of(value)
。of
に null
を渡すと NullPointerException が発生。Optional.ofNullable(value)
では null だと空オプショナルを返す。
Optional を返すメソッドで null
を返してはならない。
ストリームの終端操作の多くはオプショナルを返す。
利点
orElse()
でデフォルト値を設定できるorElseThrow()
で例外をスローする
get()
orElseGet()
filter
map
flatMap
ifPresent
isPresent()
で返る boolean 値で独自の処理を記述するjava9 で Optional に stream()
メソッドが追加されてストリームに変換できるようになった。
コレクション、ストリーム、マップ、配列、オプショナルを含むコンテナ型はオプショナルで包むべきではない。
Optional 生成分のコストはあるのでパフォーマンスがシビアな場合は使わない。
基本データ型をボクシングして Optional にするのはコストが高いので OptionalInt, OptionalLong, OptionalDouble が提供されている。
キー、値、あるいはコレクションや配列の要素としてオプショナルを使うことは大抵適切でない。 戻り値以外でオプショナルを使うことは少ない。
Javadoc でコードにドキュメントコメントを埋め込むことでドキュメントを自動生成できる。
全ての公開されているクラス、インターフェース、コンストラクタ、メソッド、フィールドの宣言の前にドキュメントコメントを書かなければならない。シリアライズ可能なら、シリアライズ形式も。
public のクラスはデフォルトコンストラクタを使うべきではない。ドキュメントできないから。 公開されていない要素についてもドキュメントコメントを書くことが望ましい。
@throws
タグによって記述@param
タグで影響を受けるパラメータと一緒に記述することもできる@param
, @return
, @throws
を書く。
@return
を省略できる@throws
は if から始まる{@code}
でコードを埋め込む。<
などの HTML をエスケープできる。ただし、@
は自分でエスケープする必要がある。@implSpec
でサブクラスへの契約を示す
-tag "implSpec:a:Implementation Reqquirements:"
をコマンドラインに渡す{@literal}
タグで囲むことでエスケープができる
{@literal}
を使ってエスケープするべき{@index}
タグで用語の索引が使えるpackage-info.java
に書くべきmodule-info.java
{@inheritDoc}
: コメントの一部を継承できるが、使いにくい。for-each ループはイテレータを隠蔽する。Iterable インタフェースを実装したオブジェクトに適用できる。配列も、コレクションも同じように扱える。 for-each ループはコンパイル時には for ループと同じようになるためペナルティはない。
for-each ループが使えない状況
特に重要なのは、java.lang
, java.util
, java.io
とそのサブパッケージ
float と double は、主に科学計算と工学計算のために設計されている。正確とは限らないので、金銭計算には特に使うべきではない。
金銭計算には BigDecimal
か int, long を使う。
BigDecimal は不便で遅い。パフォーマンスが重要なら、int か long で小数点の位置を自分で管理しながら実装する。 9桁までは int, 18 桁までは long, それ以上は BigDecimal
両者の違い
ボクシングされた基本データに対する ==
はオブジェクトの比較を行うので間違えやすい。明示的にアンボクシングすれば解決。
ボクシングされた基本データと基本データ型を一緒に使うと、大抵の場合はアンボクシングされる。null では NullPointerException が発生する可能性がある。
ボクシングされた基本データの使い道
文字列の +
は1行の生成や、小さな固定サイズの文字列の構築には向いている。
しかし、一般に n 個の文字列を結合するのに O(n^2)
の時間を必要とする。文字列は不変であるのでコピーが毎回走るから。
StringBuilder を使う。
パラメータ、戻り値、変数、フィールドは可能な限りインタフェース型で宣言する。 具体的なクラスを参照するのはコンストラクタを呼ぶ時だけ。 それによって柔軟性が得られる。
同じインタフェースを使う場合でもインタフェースで定義されている以上の契約に依存する場合は注意が必要。
インタフェース型がない場合はそのクラスで宣言するのも良い。
コアリフレクション機能 : java.lang.reflect
デメリット
代表的な利用用途:コード解析ツール、依存性注入ツール 大抵の場合はリフレクションは必要ない。
リフレクションでインスタンスの生成のみを行い、メソッドなどへのアクセスはインタフェースやスーパクラスを通して行う。
JNI : Java Native Interface C や C++ のネイティブメソッドを呼ぶ
ネイティブメソッドの利用用途
デメリット
早すぎる最適化は悪。 速いプログラムよりも優れたプログラムを パフォーマンスを制限するような設計はしない。モジュール間や外部とのやり取りの API やプロトコル。
パッケージ名とモジュール名は、ピリオドで区切られた要素で階層的であるべき。逆順のドメイン。java, javax は例外。パッケージ名は 8 文字以下が好ましい
is
で始まる。稀に has
で始まる。そのあとに形容詞句として機能する、名詞、名詞句、単語か句が続くget
から始まるものか、それ自体の名詞を返すto
で始まる。toString など。レシーバーオブジェクトの型とは異なるビューを返すときは、 as
から始まる。asList など。基本データを返すとき、Value
で終わる。intValue など制御フローとして例外を使わない。API を設計するときも、通常の制御フローに例外を使わない。検査メソッドを提供するべき。または、空のオプショナルか null を返して戻り値で区別する。 並行処理の干渉の可能性があるときは戻り値での区別を選択する。大抵の場合は検査メソッドを提供するのがいい。検査メソッドを忘れて呼び出した時に例外を発生させるとバグに気付きやすくなる
3 種類の例外
呼び出し元が適切に回復できるような状況に対してはチェックされる例外を使う。catch を強制できる。これは呼び出しの結果としてエラーが起こる可能性があることを表明することになる。
後者2つはチェックされない例外。振る舞いは同じ。一般にはキャッチするべきではない。
プログラミングエラーを表す時に、実行時例外を使う。事前条件違反。 プログラムエラーなのか、回復可能な状態を扱っているのかが必ずしも明らかではない。
エラーは、JVM のために予約されている。JVMの実行な不可能な場合、資源不足などで発生する。Error
サブクラスは作らない。AssersionError 以外の Error をスローしない。
チェックされない例外は、RuntimeException
をサブクラス化する。
追加の情報を付与するためにフィールドやメソッドを例外に実装する。文字列に含めない。
チェックされる例外は使う側に負担。 ストリームではチェックされる例外は使えない。
チェックされる例外は、適切に API を使った時に防ぐことができず、かつ、ユーザーがそれに対して何らかの有用な処理を行うことができる場合にのみ利用する。
新しくチェックされる例外を追加することは大きな変化であり、避けたい。オプショナルを返すことで回避可能。ただし、詳細な情報を返すことができない。 例外がスローされるかを検査する boolean のメソッドを追加することで、チェックされる例外をチェックされない例外に変換する。ただし、並行処理での状態遷移の可能性に注意する
コードの再利用性は大切。
Exception, RuntimeException, Throwable, Error を直接使わない。抽象クラスのように扱う。
よく再利用される例外
例外 | 使う機会 |
---|---|
IllegalArgumentException | null ではないがパラメータ値が不適切 |
IllegalStateException | メソッド呼び出しに対してオブジェクト状態が不正 |
NullPointerException | パラメータ値が禁止されている null |
IndexOutOfBoundsException | インデックスパラメータ値が範囲外 |
ConcurrentModificationException | 禁止されているオブジェクトの並行した変更を検出 |
UnsupportedOperationException | オブジェクトがメソッドをサポートしていない |
そのほかの既存の例外を再利用することも可。その例外のドキュメンテーションと矛盾しない、名前、セマンティックスに基づいている。 また、既存の例外をサブクラス化して拡張するのも可。ただし、例外はシリアライズ可能であることに注意。
IllegalArgumentException
と IllegalStateException
の区別は、どんな引数を渡してもうまく動作しないときは IllegalStateException
下位のレイヤーからの例外をそのまま再利用するのではなく、そのレイヤーの抽象度に適した例外に変換してスローする。実装の詳細で汚染されてしまう。例外翻訳(exception translation)
下位の例外のコンテキストが必要な場合、例外連鎖を行う。上位の例外が Throwable をコンストラクタで引き受ける時に可能。 連鎖可能なコンストラクタを持たない場合は、Throwable の initCause を使う。
最善なのは、例外翻訳を乱用するのではなく、例外が発生しないように事前に検査をすること。または、上位レイヤで例外処理をして例外を発生させないこと。
@throws
を使って Javadoc に各例外がスローされる条件を正確にドキュメント化する。スーパークラスをスローするような手抜きはダメ。main
メソッドは例外で Exception をスローすると記述せざるを得ない。
チェックされない例外についてもドキュメント化することが賢明。事前条件を記述することにもなる。ただし、チェックされない例外については @throws
に書いて、throws
には追加しない (?)
インタフェースでもチェックされない例外を文書化することで一般契約の一部を表せる。
クラス内で共通の例外を発生させるときは、クラスのドキュメンテーションにかく。(NullPointerException) など
スタックトレースには、例外の toString()
の結果が含まれる。のちの分析のための情報(例外の原因となったすべてのパラメータとフィールドの値)をエラーの詳細情報に記録するべき。
セキュリティに関わることは含めない。パスワードや鍵など
不必要に長くしない。
エンドユーザへのエラーメッセージと混同しない。
パラメータを例外のコンストラクタで受け取ってメッセージを生成するのもあり。
失敗したメソッド呼び出しはオブジェクトをそのメソッド呼び出しの前の状態になっているべき。(そのエラーが回復することを期待されているとき)
不変オブジェクトなら簡単。 可変オブジェクトなら
エラーは一般に回復不能。エラーアトミック性を頑張る必要はない。 エラーアトミック性が破られるようなときはドキュメンテーションする
空の catch ブロックはダメ。
無視することが適切な場合もある。回復が必要ない場合など。無視するときは、エラー変数名を ignored
にして無視する理由などをコメントする
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)として共有できる。
過剰な同期は、パフォーマンス低下、デッドロック、予想外の振る舞いを引き起こす可能性がある
活性エラー(通信効果)と安全性エラー(データ不整合)を避けるために、同期されたメソッドやブロック内で制御をクライアントに譲らない。オーバーライドされるように設計されたメソッドや関数オブジェクトを同期された領域内で呼び出さない。それらのメソッドは異質(alien)である。
Java の synchronized は再入可能(reentrant)。同一スレッド内でネストしてロックを獲得することができる。ただし、別スレッドで synchronized を待ち合わせるとデッドロック。 再入可能だが、活性エラーを安全性エラーに変える可能性がある。
異質な呼び出しを同期ブロックの外で行うことで解決する(オープンコール)。コピーを取るときは、コンカレントコレクションが提供する CopyOnWriteArrayList が便利。通常はパフォーマンスが悪いが、滅多に変更されずに走査されるだけの時には有用。
オープンコールは、デッドロックを避けるだけでなく、不必要に長いロックを防ぐ。
同期された領域内での処理は最小限にする。同期のコストは、ロックの獲得ではなく競合の方が大きい。
可変クラスを並行に使えるようにする方法
基本は前者。後者に明確なメリットがない場合は、クラスは同期しないでスレッドセーフでないことをドキュメント化する。 内部的に同期するときのヒント:ロック分割、ロックストライピング、非ブロッキング並行性制御
static なフィールドはグローバルである。
エグゼキュータサービスは便利。特定のタスクの待ち合わせ、すべてのタスクやあるタスクグループの待ち合わせなど様々なことができる。 スレッドプールから、複数のエグゼキュータサービスを生成することもできる。
Executors.newCachedThreadPool
: 小さいプログラムや軽い負荷のサーバ
Executors.newFixedThreadPool
: 固定数のスレッドを持つプールを提供するThreadPoolExecutor
を操作する。Thread は処理の単位と実行する機構の2つの役割 エグゼキュータサービスでは分離されている。処理の単位はタスクと呼ばれる。Runnable と Callable。Callable の方は値を返せるし例外をスローできる。 実行する機構がエグゼキュータサービス
fork-join はタスクの steal をする
java.util.concurrent の高レベルのユーティリティ3つ
wait と notify は同期された領域内で使う必要がある。条件変数的な使い方
wait は条件を検査する while ループの中(wait ループイディオム)で使う。ループの外で呼び出してはいけない。活性を保証するために待ちの前に検査することは必要。安全性を保証するために待ちの後に検査することは必要。
synchronized が使われているからといってスレッド安全とは限らない。
スレッド安全性のレベル
一般にはクラスにドキュメントするが、特別なスレッド安全性特性を持つメソッドはメソッドコメントにドキュメントする。
enum の不変性をドキュメント化する必要はない。
Collections.synchronizedMap のように static ファクトリメソッドは返されるオブジェクトのスレッド安全性をドキュメント化する
誰もがアクセス可能なロックを使って長期間ロックを確保すると DoS に弱い。回避するためにプライベートロックオブジェクトを使う。final で宣言する。
private final Object lock = new Object();
public void foo() {
synchronized(lock) {
// do something
}
}
ただし、条件付きスレッドセーフではドキュメント化する必要があるから使えない。
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 を取っても良い。きわどい単一チェックイディオムと呼ばれる。
プラットフォームによってスレッドスケジューラのポリシーは異なる可能性がある。移植性を保つために依存しない。
頑強で応答性がよく、移植可能なプログラムのために、実行可能なスレッドの平均数をプロセッサの数よりもはるかに大きくしない。スレッドスケジューラのできることがすくなくなる。 有益な処理をしないときは、待たせる。ビジーウェイトをしない。
スレッドの CPU 時間が短いことの解決に Thread.yield を(スレッド実行権の放棄)使わない。 スレッドの優先順位の調整は移植性がなくなるのでしない。
シリアライズのデメリット:見えないコンストラクタ、APIと実装間の曖昧な境界、セキュリティやパフォーマンス、正しさの問題
根本的な問題:攻撃対象領域が広すぎて保護できない。
シリアライズ可能な型をガジェットと呼ぶ。 ディシリアライズに時間がかかるものを送りつける DoS 攻撃。
信頼できないバイトストリームをディシリアライズしない。クロスプラットフォーム構造化データ表現を使う。JSONや protobuf
どうしても避けられないときは、ディシリアライズフィルターを使う。 java.io.ObjectInputFilter
。ブラックリストよりもホワイトリストを使う。
implements Serializable
を追加するだけでシリアライズ可能になる。ただし、一度リリースされると互換性のために大変になる。
private なフィールドも含まれるために公開 API の一部になってしまう。
コスト
気をつけること
サブクラスがシリアライズ可能であるためには継承元がシリアライズ可能であるか、パラメータなしのコンストラクタが必要。
内部クラスは Serializable にしない
適切かどうかを最初に検討せずに、デフォルトのシリアライズ形式を受け入れてはいけない。
デフォルトのシリアライズ形式は、そのオブジェクト内に含まれるデータとそれに紐ずく到達可能なすべてのオブジェクトに含まれるデータを記述する。
オブジェクトの物理表現と論理的内容が同じ場合、デフォルトのシリアライズ形式はおそらく適切。
デフォルトのシリアライズ形式が適切であっても、不変式とセキュリティを保証するために多くの場合 readObject メソッドを提供しなければならない
デフォルトのシリアライズ形式を使うことのデメリット
効率的な writeObject と readObject を提供する。 同期にも気をつける 明示的なシリアルバージョンUIDを宣言する
private static final long serialVersionUID = xxxxxxx;
食い違うと InvalidClassException が発生する
readObject メソッドは実質的にもう1つの public のコンストラクタ。 防御的コピーを使っている不変クラスでは、readObject でも引数の妥当性と防御的コピーをする必要がある。
不正なバイト列を防ぐために、readObject で不変式が満たされているか正当性の検査をする。不正な場合は、 InvalidObjectException また、防御的コピーもする
クラス内のオーバーライド可能なメソッドを呼び出してはいけない。
readResolve で単一インスタンスのみを再利用することを強制できる。無視されたインスタンスは GC される。
シングルトンのインスタンスの転送ではフィールドの値は必要ないから、transient と宣言する。然もなくば脆弱性
enum によって安全になる。
Serializable を実装することはバグとセキュリティ問題の可能性を増大させる。 コンストラクタ以外でインスタンス化されるから。
シリアライズ可能なクラスの private static のネストしたクラス(プロキシ)を導入。コンストラクタでは、一貫性検査や防御的コピーは必要ない。 エンクロージングクラスに writeReplaace メソッドを追加して、プロキシを挟む。readObject は不変式の破壊を防ぐために潰す。 プロキシクラスの readResolve でエンクロージングクラスに変換する。このときはエンクロージングクラスの public なインタフェースでインスタンス化する。
ユーザによって拡張可能なクラスとは互換性がない。循環を含むようなクラスとも互換性がない場合がある。
プロキシを使うことでパフォーマンスが犠牲になることがある。
amazon