不可变对象应该使用直接字段访问来保持一致性,因为它允许设计对象完全按照客户期望的方式执行。
考虑一个系统,其中每个可变字段都隐藏在访问器后面,而每个不可变字段都没有。现在考虑以下代码片段:
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()
,您只能假设正在发生以下三件事之一:
- 没有。该字段
children
按原样返回,您不能修改列表,因为您会破坏 Node.js 的不变性。为了修改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
,直接字段访问就允许对象发生变异,从而迫使您的对象层次结构被仔细设计为从上到下不可变。
好消息是,您不仅获得了不变性的所有好处,还能够推断出无论在何处访问字段的性能特征,而无需查看实现并确信它永远不会改变。