【问题标题】:How to find out if a class is immutable如何判断一个类是否不可变
【发布时间】:2018-02-04 21:36:54
【问题描述】:

在具体化我的问题之前,让我提供一些背景知识:我的主要编程语言是 C++ 和 Java。在使用 C++ 时,我发现应用 const 正确性很重要,即声明如下函数:

A::DoSomething( const B& arg ); // guarantees that arg is not modified
A::DoSomething() const; // guarantees that the object of type A is not modified
A::DoSomething( const B& arg ) const; // both of the above

事实上,我经常希望const 是默认值,并且必须以某种方式标记已修改的对象。

我使用const的主要原因是:

  • 与其他开发人员的交流:它使代码更具表现力。
  • 与编译器的通信:它有助于在编译时发现问题,有时还可以进行其他优化。

众所周知,Java 没有 const 关键字(您不能使用 final 进行上述操作),这个事实之前已在此处讨论过,请参见此处的示例:Equivalent of const(C++) in Java

通常建议的 Java 替代方案是使您的类不可变。虽然这不是 const 的完全替代品,因为它适用于每个类而不是每个使用类的上下文,但在大多数情况下它对我来说都可以正常工作。

但是不可变类有一个大问题:不可变性并不明显。要确定一个类是否真的不可变,据我所知,你基本上必须检查完整的源代码。任何方法都可能有一个后门,通过该后门可以修改对象。

那么有没有更简单的方法来检查不可变性?或者是否有任何最佳实践以某种方式将类标记为不可变?

注意:我知道这两种语言都提供了绕过常量或不变性的“邪恶技巧”:C++ 有 const_cast,而 Java 有反射。但是对于我的问题的上下文,我们假设没有使用这些。

【问题讨论】:

标签: java oop immutability


【解决方案1】:

Java 没有一流的不变性支持,因此您没有可靠的方法来检测类是否不可变。

Java Concurrency In Practice 推荐(参考附录 A)使用来自javax.annotation.concurrent 的类级别的@Immutable 注释,这是表示不变性的最方便、最常用和标准的方式;自定义 javadoc 也很好。请注意,它只是声明,而不是实际约束。

类的设计也是一个很好的指标:

  • 只有最终字段(但在极少数情况下,可能有一些非最终字段并且仍然是不可变的,例如,请参阅 String#hashCode)
  • 构造正确(this 引用不会从构造函数中泄漏)
  • 对象状态不能被修改(所以类不应该有setter和mutator方法)
  • 不要存储对可变对象的外部(传递给构造函数)引用(例如,创建传递的集合参数的防御性副本)

不可变类设计属性的完整列表可以在Oracle tutorials 中找到。

因此,要检查类是否不可变,您首先查看类级别注释和 javadoc,然后再查看实现本身。

为了提供额外的完整性检查(如果您认为注释为不可变类可能会错误地可变),有一个用于 FindBugs(静态分析工具)的 Mutability Detector 插件,它可以有效地执行上面列出的相同操作:检查类有@Immutable 注释并验证(通过反射)所有不变性规则都得到满足(还有一些额外的东西,比如来自 Guava 的不可变集合支持等)。可变性检测器也可以用作没有 FindBugs 的库,它允许您编写这样的测试:

@Test
public void testImmutable() {
    assertImmutable(MyClass.class);
}

【讨论】:

  • 我的代码中出现带有@Immutable 注释的编译器错误 - cannot find symbol @Immutable symbol: class Immutable 1 error
  • @roottraveller 可以在JSR305中找到@Immutable注解:<dependency> <groupId>com.google.code.findbugs</groupId> <artifactId>jsr305</artifactId> <version>3.0.2</version> </dependency>
  • 关于final 字段:这只保证reference不会被更改。但是,可变字段(例如java.util.List)的仍然可以更改。
  • Java 现在有record。唉,它还没有(还没有?)声明@Immutable
【解决方案2】:

根据Java documentation

  • 不要提供“setter”方法——修改字段或字段引用的对象的方法。
  • 将所有字段设为最终字段和私有字段。
  • 不允许子类覆盖方法。最简单的方法是将类声明为 final。更复杂的方法是将构造函数设为私有并在工厂方法中构造实例。
  • 如果实例字段包含对可变对象的引用,请不要更改这些对象:
    • 不要提供修改可变对象的方法。
    • 不要共享对可变对象的引用。永远不要存储对传递给构造函数的外部可变对象的引用;如有必要,创建副本并存储对副本的引用。同样,在必要时创建内部可变对象的副本,以避免在方法中返回原始对象。

实施不变性的最简单方法是通过 Lombok 库中的 @Value 注释。

如果您使用 IntelliJ,您可以检查上述项目符号的类字节码

【讨论】:

  • 这不是一个很好的例子,因为 Lombok @Value 仅帮助您拥有一个不可变的类,但 Lombok 的实现是不完整的(至少在撰写本文时)。在您的情况下,即使private finaljava.util.Date 字段仍然是可变的:Lombok 生成的构造函数不会克隆日期,因此可以在外部对其进行修改,例如Date d = new Date(); FinalClass fc = new FinalClass(1,"",d); d.setTime(0);。参见projectlombok.org/features/ValueString[] 参数未在构造函数中克隆,因此可以在之后修改其内容。
  • 将字段设置为final 并不能保证使其不可变。这只是不可变的 reference,而不是它的 value(请参阅您的 java.util.Date 字段)。反之亦然:即使不是final,您的字段也可能是不可变的(请参阅不可变类StringhashCode() 方法的缓存值)。
  • 这没有回答 OP 问题(如何确定一个类是否不可变?)。
猜你喜欢
  • 2013-06-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-17
  • 2012-06-07
  • 2011-05-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多