ドメイン: Java のルール
昨日の入力に最適化されたパスは、今日の入力が来ると非最適化される [R:a-speculated-path-deoptimises-when-the-input-changes]
Aleksey Shipilev「JVM Anatomy Quarks」のプロファイル汚染、インライン化、脱最適化に関する記述、およびHotSpotのアンコモントラップ機構に基づきます。
JITはコードをコンパイルしません。コードに関する賭けをコンパイルし、その賭けが成立しなくなった時点で決済されます。
HotSpotはプロファイルに基づいてコンパイルします。このコールサイトはこれまでに1つのレシーバ型しか見たことがないのでインライン化してガードする、この分岐は一度も取られていないので出力しない、このフィールドは一度もnullになっていないのでチェックをスキップする、このクラスは一度もロードされていないのでサブクラスが存在しないと仮定する。各仮定にはガードが設けられ、コンパイルされたパスが高速なのは、まさにガードが安価で、本体が存在しないからです。ガードが失敗すると、フレームは非最適化されます。アンコモントラップが発生し、コンパイルされたフレームからインタプリタフレームが再構築され、実行はインタプリタで継続され、新しいプロファイルで再コンパイルされるのを待ちます。
タイミングこそが、これをスループットのルールではなくレイテンシのルールにしている理由です。仮定が破られるのは入力が変わるときです。つまり、通常と異なる注文タイプ、その日の最初の例外、新しい会場のメッセージフォーマット、6時間目にロードされたインターフェースの2番目の実装などです。したがって、パスが生み出す最悪のレイテンシは、最も普通でないイベントで発生し、それは一般的に最も重要だったイベントであることが多いのです。平均的な代表的なトラフィックでの負荷テストでは、これが再現されることはありません。
また、デフォルトでは静かです。何もログに記録されず、メトリクスも動かず、唯一の痕跡は、コレクションポーズやスケジューリングの遅延と区別がつかないレイテンシの外れ値です。-XX:+PrintCompilation と -XX:+TraceDeoptimization で何が起こったかがわかります。JFRはより低いオーバーヘッドでイベントを運び、有効にしておく価値があります。
したがって、通常のケースだけでなく異常なケースでもパスをウォームアップしてください。これは [R:a-measurement-matches-the-regime-it-reports] と関連します。典型的なトラフィックのみから構築されたウォームアップは、典型的なトラフィックによって確認され、非典型的なトラフィックによって破壊されるプロファイルを生成します。ホットコールサイトは意図的にモノモーフィックに保ってください。ホットパス上のメガモーフィックなインターフェースコールは、1回限りのトラップではなく、永久的なインラインバリアとなります。パスが生きている前に、パスが必要とするクラスをロードしてください。そして、ホットパスに追加されたラムダや動的コールサイトは、再び獲得しなければならない新しいプロファイルとして扱ってください。
障害モードチェック:JITはこのパスについて何を仮定しているか、そしてそれらの仮定の1つが最初に偽になったとき何が起こるか? 正直な答えが、何が仮定されたか誰も知らないということなら、それが発見です。仮定は読み取り可能であり、それを読み取ることが作業です。
コレクションビューは誰か他のデータへの窓である [R:a-view-is-not-a-copy]
自分がデータを保持しているのか、データへの窓を保持しているのかを知ってください。どちらの場合も静的型は List<T> であり、決して教えてくれません。
ライブラリは、ほとんどのコードが想定するよりもはるかに頻繁にビューを返し、それぞれに独自の驚きがあります:
Arrays.asList(a)は、配列に対する固定サイズのラッパーです。setは配列に書き込みます。addとremoveはUnsupportedOperationExceptionをスローします。List.of(...)は不変で独立しており、通常は意図されたものです。list.subList(from, to)はライブウィンドウです。書き込みは通過し、親への構造的な変更があると、サブリストは次回の使用時にConcurrentModificationExceptionをスローします。これには、サブリストを取得してから親をクリアするという一般的なイディオムも含まれます。map.keySet(),values(),entrySet()はビューです。keySet().remove(k)はマップからエントリを削除します。これは時々望まれることであり、常に意識的に行う価値があります。Collections.unmodifiableList(l)はコピーせずにラップします。ラッパーは書き込みを拒否し、元のリストはまだ機能するため、呼び出し元からは防御しますが、自分自身からは防御しません。これは防御的コピーではないため、[R:no-reference-to-internals-escapes]では代わりにList.copyOfが使用されます。Stream.toList()は変更不可能です。collect(Collectors.toList())はそのような保証を提供せず、変更可能かもしれないし、そうでないかもしれません。どちらも可変であると想定しないでください。
したがって、作成時にどれが必要かを明示してください。受信者が独立していなければならない場合は List.copyOf、共有が意図されておりライフタイムが短い場合はビューを使用します。フィールドに保存されたビューは、最も注意深く検査する形状です。それはそれを作成した式よりも長生きし、バッキングコレクションはその後いつでも変更される可能性があります。
同じ区別がイテレーションにも適用されます。イテレーション中にコレクションを変更すると、シングルスレッドであっても ConcurrentModificationException がスローされます。イテレータもビューであるためです。removeIf を使用するか、コピーをイテレートするか、削除を収集して後で適用してください。
障害モードチェック(受け取った、または返されたコレクションごとに):これに誰かが書き込んだ場合、他に何が変わるか? ファクトリメソッドのJavadocを開かずに答えられない場合は、コピーしてください。
ラッパー型は、エスケープ分析によって除去されるまで、1つのオブジェクトのコストがかかる [R:a-wrapper-type-is-not-free-here]
ドメインの概念にはここでも独自の型を与えてください。OrderId を AccountId が属する場所に渡せないようにすることは、Javaでも他の言語と同じくらい価値があります。ただし、設計の議論と共にコストの議論を持ち込まないでください。これらのうち片方しか適用できないからです。
Rustでは、newtypeはコンパイル時の構文です。struct OrderId(u64) は実行時には u64 であり、コンパイラがそれを証明します。Javaでは、record OrderId(long value) はオブジェクトです。ヘッダ、フィールド、追跡する参照、そしてそれを指していたものとは別のキャッシュラインです。JITはそれを除去できます。エスケープ分析とスカラー置換により、コンパイル単位からエスケープしないことが証明可能なラッパーはフラット化されます。これは多くの通常のコードをカバーします。しかし、それがカバーしないのは、あなたが気にするケースです。フィールドに保存され、コレクションに入れられ、JITがインライン化を拒否したメソッドをまたいで渡され、または配列内にあるラッパーです。配列では、OrderId[] はポインタの配列であり、long[] は数値の配列です。
したがって、値がどこに存在するかによって規律が分かれます:
- API境界、および正確性が懸念される場所では、ラッパーを使用してください。これが防ぐ引数入れ替えバグは、通常JITが除去するアロケーションに見合う価値があります。
- ホットループ内、大きな配列内、またはメッセージごとの構造体内では、ラッパーが消えたと仮定する前に測定してください。これは
[R:verify-the-abstraction-compiled-away]が、最適化の決定が動的である言語で現れるものです。-XX:+PrintInlining、JFRアロケーションプロファイル、またはエスケープ分析ダンプが実際に何が起こったかを示し、同じバイナリの実行間でも答えが異なる可能性があります。 - ラッパーが消えず、予算が現実的である場所では、プリミティブを保持し、コストがかからない場所に安全性を置いてください。名前付きパラメータ、ビルダー、シグネチャが誤って呼ばれない静的ファクトリなどです。
来るべきこのルールのバージョンは、待つのではなく知っておく価値があります。Project Valhallaの値クラスは、最適化の裁量ではなく仕様によって、まさにこのラッパーをフラット化するように設計されています。それが真であるJVM上でコードが実行されるまでは、コストは動的であり、想定するのではなく測定する必要があります。
障害モードチェック(予算のあるパスでプリミティブをラップする前に):このオブジェクトはJITが終了した後もまだ存在するか、そして何がそれを教えてくれたか? 「ただのラッパーだ」はRustの答えであり、それは移行できない答えです。
リソースは、それを開いた構文によって、すべてのパスで閉じられる [R:close-what-you-open]
リソースは try-with-resources ヘッダーでのみ開き、他の場所では開かないでください。この構文は、すべての出口で、逆順で閉じます。そして、手書きの finally ではできないこととして、クローズも失敗した場合に元の例外を保持します。
try (var conn = pool.take(); var stmt = conn.prepare(sql)) {
return stmt.execute();
}
finally ブロックがもたらす失敗は、正しく見えるため、明確に述べる価値があります。本体がスローし、close() もスローする場合、finally の例外が本体の例外を置き換えます。プログラムは接続のクローズ失敗を報告し、最初に失敗したクエリについては何も言いません。代わりに、try-with-resources はクローズの失敗を抑制し、元の例外に添付します。そのため、両方が生き残り、原因はあなたが望んでいたものになります。リークはハンドルを失わせます。原因の喪失は調査を失わせます。
この構文が適用されないが、規律は依然として適用される3つのケース:
- ライフタイムがブロックではなくフィールドであるリソース(プール、クライアント、エグゼキュータなど)。所有オブジェクトは
AutoCloseableになり、規律は1つ上のレベルに移動します。拒否すべきは、開かれて、特定の誰によっても閉じられないフィールドです。 ExecutorServiceはshutdown()だけでは閉じられません。shutdown()の後にawaitTermination()、次にshutdownNow()の順序で行う必要があり、これをスキップすると、デーモンスレッドではないスレッドがJVMを保持し続けます。- ファイル上の
Stream(Files.lines、Files.walk)はハンドルを保持し、閉じる必要があります。これはリークする唯一のストリームであり、リークしないストリームとまったく同じように見えます。
ファイナライザや Cleaner を主要なメカニズムとして使用しないでください。これらは不特定の時間または決して実行されず、規律がすでに失敗した場合をキャッチするために存在します。
障害モードチェック(すべてのリソースに対して):次の行がスローした場合、どの構文がこれを閉じるか? 答えがメソッドのさらに下にある close() 呼び出しである場合、何も閉じません。
クラスは final にするか、その継承が設計および文書化されている [R:design-for-inheritance-or-forbid-it]
サブクラスのために設計していない限りクラスを final にし、もし設計した場合は、サブクラスが正確に何をオーバーライドしてよく、何をしてはいけないかを明示してください。
継承は、見た目ほどの再利用メカニズムではありません。サブクラスは、スーパークラスのメソッドのうちどれがどれを呼び出すかに依存します。addAll が add を呼び出すのは標準的な例です。これは、スーパークラスがパッチリリースで変更する権利を持つ実装の詳細です。したがって、動作していたサブクラスは、親がリファクタリングされたときに壊れます。どちらの作者も何も悪いことをしていないのにです。コンポジションにはこの特性はありません。ラッパーはパブリックコントラストにのみ依存し、他には依存しません。
コンストラクタのケースは、脆弱であるどころか、最初から壊れています。オーバーライド可能なメソッドを呼び出すコンストラクタは、サブクラスの独自のフィールドが割り当てられる前にサブクラスのオーバーライドを実行するため、オーバーライドは、final として宣言され初期化されたフィールドに null と 0 を認識します。何も警告しません。これは、[R:no-reference-to-internals-escapes] が this のエスケープとして挙げているものと同じ公開ハザードであり、通常のメソッドディスパッチのように見えるルートを介して到達します。そして、[R:publish-safely-or-not-at-all] が、スレッドが1つしかない場合でもこれが重要である理由です。
したがって:
- デフォルトで
finalにします。クラス、およびサブクラスが置き換えるべきでないメソッドは。Sealing([R:seal-the-alternatives])は、サブタイプのセットが閉じられて既知である、より強力な形式です。 - コンストラクタ、イニシャライザ、または
clone()やreadObject()(これらは別の名前をまとったコンストラクタです)からオーバーライド可能なメソッドを決して呼び出さないでください。 - クラスが継承のために設計されている場合、自己使用を文書化してください。どのメソッドがどれを呼び出すか、どれがオーバーライドしても安全か、サブクラスが何を呼び出さなければならないか。その文書は契約の一部であり、そのコストは、ほとんどのクラスが代わりに
finalであるべきである正直な理由です。 - コンポジションを優先してください。 インスタンスを保持して転送するラッパーは記述が長くなりますが、親の内部実装に影響されず、動機が代替可能性ではなく再利用であった場合に使用するものです。
障害モードチェック(finalでないすべてのクラスに対して):私のメソッドのうちどれが互いに呼び出しているか、そしてサブクラスがそのうちの1つをオーバーライドしても、私がそれらを並べ替えた後でも正しいままか? それに答えられない場合、そのクラスは偶然に拡張可能になりました。
equals と hashCode は一つの決定であり、可変キーは両方を破壊する [R:equality-is-one-contract]
等価性を一度決定し、両方の半分を同じ変更で実装し、オブジェクトがコレクションから到達可能な間は変更できないフィールドからそれらを導出してください。
契約は機械的です。等しいオブジェクトは等しいハッシュコードを持たなければなりません。それを破ると、HashMap は間違ったバケットを探すため、get は含まれているキーに対してnullを返し、remove は削除するエントリを見つけられません。これはトラフィックとともにサイズが大きくなるリークです。何もこれを検出しません。コレクションは指定されたとおりに正確に動作しています。仕様は嘘をつくオブジェクトを渡されました。
可変キーの半分はレビューを生き残るものです。なぜなら、クラスは書かれた時点では正しく、誰かがセッターを追加したときに間違ったものになるからです。後で変更されるフィールドに hashCode が依存するオブジェクトは、マップに通知されることなくバケットを移動し、それが提出されたまさにそのキーを通して到達不能になります。したがって、キーとして使用される型は不変であるか、キーとして使用されません。 record は、コンポーネントから生成された両方の半分と、構築による final フィールドを提供します。これが値型のデフォルトの形状であり、これら2つのメソッドを手書きすることが定型コードではなく決定のように感じられるべき理由です。
同じ契約から派生し、それぞれ独自の失敗の仕方があるため、述べる価値があるさらに2つ:
compareToはequalsと一致しなければなりません。 型がTreeMapまたはソートされたセットに到達する場合、これらは順序付けを使用し、equalsをまったく呼び出しません。コンパレータが2つのオブジェクトを等価とみなす一方で、equalsが異なると言う場合、セットはそのうちの1つを静かに保持することになります。- 継承と等価性は合成できません。
instanceofで書かれたequalsはサブクラスを受け入れ、非対称になります。getClass()で書かれたものは対称的で、すべてのサブクラスを拒否します。3番目のオプションはありません。これは、[R:design-for-inheritance-or-forbid-it]がfinalを好む理由の1つです。
障害モードチェック(型がキーとして使用されたり、セットに入れられる前):その等価性はどのフィールドに依存するか、そしてそれらのフィールドのうち、オブジェクトがコレクション内にある間に変更できるものはあるか? 答えが「はい」の場合、その型は不変ではないため、キーとして使用できません。
例外は、何が失敗したかを命名するのであり、何が失敗したと推定するかではない [R:exceptions-name-what-failed]
例外に、何が起こったかを伝えさせ、それがなぜ問題であるかを推論させないでください。メッセージは障害を識別します。スタックトレースが調査を開始する場所です。
問題は暗黙の因果関係です。「データベース接続の取得に失敗しました」は、障害を特定しません。障害が発生した場所を特定します(JDBCドライバ名や、障害が発生する前に何をしようとしていたかではありません)。問題のある行の前後数行のコンテキストは、読み取り、ログ、または監視に値します。
- 障害の種類を挙げてください。 「エラー: ファイル 'orders_2024-03-15.csv' を開けませんでした。」これにより、どのリソースが失敗したかが明確になります。
- 利用可能な識別子を含めてください。 「顧客 ID 1234 の注文 ID 'ORD-98765' の検索に失敗しました。」これにより、後の調査が可能になります。
- 「おそらく」や「かもしれない」で始まる推測は避けてください。 推測は、調査を誤った方向に導く可能性があります。
- スタックトレースは嘘をつきません。 それを尊重してください。
障害モードチェック:例外メッセージだけで、どのリソースが失敗したかを特定できますか? できない場合は、メッセージを改善してください。
同一性は、ボックス型にとって等価性ではない [R:identity-is-not-equality-for-boxes]
プリミティブラッパー(Integer, Long, Short, Byte, Character, Boolean, Float, Double)はオブジェクトですが、同一性ではなく値に基づいて比較する必要があります。
== は参照の同一性をチェックします。equals() は値をチェックします。
Integer a = 128;
Integer b = 128;
System.out.println(a == b); // false (異なるオブジェクト)
System.out.println(a.equals(b)); // true (同じ値)
キャッシュ(Integer の場合はデフォルトで -128 から 127)はこれを隠すことができます。== が「動作」するのは、値がキャッシュ範囲内にある場合のみです。範囲外では、期待どおりに失敗します。一貫性がなく、信頼できません。
したがって、以下の場合を除き、プリミティブラッパーには常に equals() を使用してください。
booleanとbyteは常にキャッシュされるため、==は安全です。ただし、習慣としてequals()を使用することをお勧めします。- パフォーマンスが重要なパスでは、最初にプリミティブの使用を検討してください。
障害モードチェック:ボックス型で == を使用していますか? 使用している場合、これはバグですか、それともパフォーマンス上の理由ですか?後者の場合、equals() を使用するか、プリミティブに変更することを検討してください。
内部への参照は外部に漏れてはならない [R:no-reference-to-internals-escapes]
内部の可変オブジェクトへの参照を漏らさないでください。防御的コピーを提供するか、不変のビューを提供するか、内部データを不変にします。
public class Order {
private List<LineItem> items;
// 問題あり:内部リストへの参照を漏らしている
public List<LineItem> getItems() {
return items;
}
// 良い:不変のビューを返す
public List<LineItem> getItems() {
return Collections.unmodifiableList(items);
}
// より良い:防御的コピーを返す
public List<LineItem> getItems() {
return List.copyOf(items);
}
}
これは、R:a-view-is-not-a-copy に密接に関連しています。ビューはコピーではありませんが、ここでの意図は、呼び出し元が内部状態を変更できないようにすることです。
このルールが適用される場所:
- ゲッター は、内部の可変オブジェクトへの参照を漏らしてはなりません。
- コンストラクタとセッター は、外部から受け取った参照を保存してはなりません。代わりにコピーを作成してください。
- 配列を返すメソッド は、配列自体への参照ではなく、配列のコピーを返す必要があります。
this参照の漏洩:R:publish-safely-or-not-at-allを参照してください。
障害モードチェック:可変状態を公開していますか? 公開している場合、呼び出し元はそれを変更できますか?変更できる場合、内部の不変性が破られています。
null は値ではない [R:null-is-not-a-value]
null は、値が存在しないことを示す特別な値です。明確に文書化し、可能な限り Optional を使用し、可能であれば null を返さないでください。
// 問題あり:null を返す可能性がある
public Order findOrder(long id) {
if (order == null) {
return null;
}
return order;
}
// 良い:Optional を返す
public Optional<Order> findOrder(long id) {
return Optional.ofNullable(order);
}
null を受け入れるメソッドは、@Nullable で明示的に文書化するか、java.util.Objects.requireNonNull() を使用して早期に失敗させてください。
- API は null を返すべきではありません。 代わりに
Optionalを使用するか、空のコレクションを返してください。 - null を引数として受け入れないでください。 代わりに
Optionalを使用するか、オーバーロードされたメソッドを提供してください。 @Nullableと@NonNullアノテーションを使用して、契約を文書化してください。
障害モードチェック:このメソッドは null を返す可能性がありますか? 返す場合、呼び出し元は null チェックを行っていますか?そうでない場合、NullPointerException が発生する可能性があります。
安全に公開するか、まったく公開しないか [R:publish-safely-or-not-at-all]
オブジェクトを公開する(別のスレッドからアクセスできるようにする)前に、そのオブジェクトが完全に構築され、安全に公開されていることを確認してください。
安全な公開のためのテクニック:
finalフィールド:コンストラクタでfinalフィールドが適切に設定されている場合、JVMはそれらが他のスレッドに正しく公開されることを保証します。volatileフィールド:volatileフィールドへの書き込みは、その前のすべての書き込みを他のスレッドに公開します。synchronizedブロック:同期化は、スレッドがモニターを取得したときに、その前のすべての変更を可視にします。finalオブジェクト内の不変オブジェクト:オブジェクトが不変でfinal参照を介して公開される場合、安全です。- スレッドセーフなコレクション:
ConcurrentHashMap、CopyOnWriteArrayListなどは、安全な公開を保証します。 AtomicReferenceまたはAtomicInteger:アトミック操作は、安全な公開を保証します。
コンストラクタから this を漏らさないでください。これは、R:design-for-inheritance-or-forbid-it で説明されているのと同じ公開ハザードです。
障害モードチェック:このオブジェクトは、複数のスレッドによってアクセスされる可能性がありますか? アクセスされる場合、どのように公開されていますか?安全に公開されていますか?そうでない場合、データ競合が発生する可能性があります。
代替案を封じる [R:seal-the-alternatives]
限られた既知の代替案のセットを持つ型の階層を設計する場合、sealed クラスまたはインターフェースを使用して、許可されるサブタイプのセットを制限してください。
public sealed interface PaymentMethod permits CreditCard, PayPal, BankTransfer {
void processPayment(Amount amount);
}
これにより、JITはより積極的に最適化でき、コンパイル時にすべてのケースをカバーできるため、より安全なコードとより高速な実行が可能になります。switch 式は網羅性チェックを提供します。
sealed クラスの利点:
- 網羅的なパターンマッチング:コンパイラは、すべてのケースがカバーされていることを確認できます。
- JIT最適化:JITは、許可されるサブタイプの完全なセットを知っているため、より良い最適化を生成できます。
- 明示的な設計:APIの設計者は、どのサブタイプが許可されるかを明示的に文書化できます。
障害モードチェック:この型の階層は、外部によって拡張されることを意図していますか? そうでない場合、sealed にすることを検討してください。
Serializable は第二のコンストラクタである [R:serializable-is-a-second-constructor]
Serializable を実装するクラスは、デシリアライゼーションが新しいインスタンスを効果的に作成するため、第二のコンストラクタを宣言します。このコンストラクタは、クラスの通常のコンストラクタをバイパスし、不変条件をバイパスする可能性があります。
public class Order implements Serializable {
private final long id;
private final List<LineItem> items;
public Order(long id, List<LineItem> items) {
this.id = id;
this.items = List.copyOf(items); // 防御的コピー
}
// デシリアライゼーションは、このクラスに適切な初期化を強制するために必要
@Serial
private Object readResolve() throws ObjectStreamException {
return new Order(id, items); // 防御的コピーを強制
}
}
Serializable は慎重に使用してください。
serialVersionUIDを常に宣言してください。 これがないと、JVMが自動生成し、クラスの変更により互換性が失われます。readObject()とwriteObject()をオーバーライドして、不変条件を強制してください。- デフォルトのシリアライゼーションフォームに依存しないでください。 クラスの内部表現は、将来変更される可能性があります。
- シリアライゼーションはセキュリティ上のリスクをもたらします。 悪意のあるデシリアライゼーション攻撃に対して脆弱性をもたらす可能性があります。
readResolve()またはwriteReplace()を実装して、シリアライゼーションフォームを制御してください。Externalizableの使用を検討してください。 シリアライゼーションフォームをより詳細に制御できます。- 可能な限り、シリアライゼーションの代わりに JSON や Protocol Buffers などの他のメカニズムを使用してください。
障害モードチェック:このクラスは Serializable を実装していますか? している場合、デシリアライゼーション中に不変条件が維持されることを確認していますか?readObject() は検証を行っていますか?serialVersionUID は宣言されていますか?