不変オブジェクトは、均一性のために直接フィールド アクセスを使用する必要があります。これにより、クライアントが期待するとおりに動作するオブジェクトを設計できるためです。
すべての変更可能なフィールドがアクセサーの背後に隠され、すべての不変フィールドが隠されていなかったシステムを考えてみましょう。ここで、次のコード スニペットを検討してください。
class Node {
private final List<Node> children;
Node(List<Node> children) {
this.children = new LinkedList<>(children);
}
public List<Node> getChildren() {
return /* Something here */;
}
}
の正確な実装を知らなくても、Node
契約によって設計するときに行う必要があるため、 が表示される場所ではroot.getChildren()
、次の 3 つのうちの 1 つが発生していると想定することしかできません。
- 何もない。フィールド
children
はそのまま返され、ノードの不変性が壊れるため、リストを変更することはできません。を変更するには、List
それをコピーする必要があります。これは O(n) 操作です。
- たとえば、次のようにコピーされます
return new LinkedList<>(children);
。これは O(n) 操作です。このリストは変更できます。
- 変更不可能なバージョンが返されます。例:
return new UnmodifiableList<>(children);
. これは O(1) 操作です。繰り返しますが、これを変更するにList
は、O(n) 操作でコピーする必要があります。
いずれの場合も、返されたリストを変更するには、それをコピーするために O(n) 操作が必要ですが、読み取り専用アクセスは O(1) または O(n) のいずれかになります。ここで注意すべき重要なことは、契約による設計に従うと、ライブラリの作成者がどの実装を選択したかを知ることができないため、最悪のケース O(n) を想定する必要があるということです。したがって、O(n) アクセスと O(n) で独自の変更可能なコピーを作成します。
ここで、次のことを検討してください。
class Node {
public final UnmodifiableList<Node> children;
Node(List<Node> children) {
this.children = new UnmodifiableList<>(children);
}
}
root.children
つまり、それは でありUnmodifiableList
、ローカルで変更可能なコピーを作成するために O(1) アクセスと O(n) を想定できます。
明らかに、後者の場合のフィールドへのアクセスのパフォーマンス特性について結論を出すことができますが、前者の場合にできる唯一の結論は、最悪の場合のパフォーマンス、つまり想定しなければならないケースがはるかに遠いということです。フィールドへの直接アクセスよりも悪い。これは、プログラマーがアクセスごとに O(n) 複雑度関数を考慮に入れる必要があることを意味します。
要約すると、このタイプのシステムでは、getter が表示される場所はどこでも、getter が変更可能なフィールドに対応するか、getter が何らかの操作を実行するか (時間のかかる O(n) 防御的コピー操作であるかどうか) をクライアントが自動的に認識します。 、遅延初期化、変換、またはその他。クライアントがフィールドへの直接アクセスを確認すると、そのフィールドへのアクセスのパフォーマンス特性がすぐにわかります。
このスタイルに従うことで、プログラマーは、対話しているオブジェクトによって提供されるコントラクトに関して、より多くの情報を推測できます。UnmodifiableList
このスタイルは、上記のスニペットを interfaceに変更するとすぐにList
、フィールドへの直接アクセスによってオブジェクトを変更できるため、均一な不変性も促進されます。したがって、オブジェクト階層を上から下まで不変になるように慎重に設計する必要があります。
良いニュースは、不変性のすべての利点を得るだけでなく、フィールドがどこにあるかに関係なく、フィールドにアクセスするパフォーマンス特性を、実装を見ずに、決して変更されないという確信を持って推測できることです。