【问题标题】:Unit testing for object immutability对象不变性的单元测试
【发布时间】:2026-01-09 03:40:01
【问题描述】:

我想确保给定的一组对象是不可变的。

我在想一些类似的事情:

  1. 检查是否每个字段都是私有的最终字段
  2. 检查类是否为final
  3. 检查可变成员

所以我想我的问题是:3. 可能吗?

我可以递归地检查一个类的每个成员是否都有其字段private final,但这还不够,因为一个类可以有一个名为getHaha(param) 的方法,例如它将给定的参数添加到一个数组中。

那么有没有一种好方法可以检查一个对象是不可变的还是可能的?

谢谢,

【问题讨论】:

标签: java unit-testing immutability


【解决方案1】:

你可能想看看这个项目:

Mutability Detector

该库尝试分析特定类的字节码,以发现它是否不可变。它允许在单元测试中测试这种情况,如视频here 中所示。它当然不是完美的(字符串字段将被认为是可变的,并且您的数组示例处理得不好)但它比 FindBugs 提供的更复杂(即仅检查每个字段是否为最终字段)。

免责声明:我写的 ;-)

【讨论】:

    【解决方案2】:

    如果您生成数据模型及其所有代码,则可以确保您创建的可能数据值对象是不可变的以满足您的需求。

    您遇到的问题是有不同形式的不变性。即使String 也会使您的测试失败Are String, Date, Method immutable? 您可以通过这种方式证明一个类是严格不可变的,但生成数据模型可能会更好。

    【讨论】:

    • 为了生成数据模型,我从一个只包含我想要的字段的类开始。编译后,我生成了填充方法、构造函数和构建器的代码。 (在模板类中字段是非最终的,在生成的类中它们是最终的)
    【解决方案3】:

    是的,您可以编写一个不变性检测器。

    首先,您不会只是编写一个确定类是否不可变的方法。相反,您将需要编写一个不变性检测器类,因为它必须保持某种状态。检测器的状态将是它迄今为止检查过的所有类的检测到的不变性。这不仅对性能有用,而且实际上是必要的,因为一个类可能包含循环引用,这会导致简单的不变性检测器陷入无限递归。

    类的不变性有四个可能的值:UnknownMutableImmutableCalculating。您可能希望有一个映射,它将您迄今为止遇到的每个类与一个不变性值相关联。当然,Unknown 实际上并不需要实现,因为它将是尚未在映射中的任何类的隐含状态。

    因此,当您开始检查一个类时,将其与映射中的 Calculating 值相关联,完成后,将 Calculating 替换为 ImmutableMutable

    对于每个类,您只需要检查字段成员,而不需要检查代码。检查字节码的想法是相当错误的。

    首先,你应该检查一个类是否是final的;类的最终确定性不会影响其不变性。相反,一个期望一个不可变参数的方法应该首先调用不可变检测器来断言传递的实际对象的类的不变性。如果参数的类型是 final 类,这个测试可以省略,所以 finality 对性能有好处,但严格来说没有必要。此外,正如您将在下面看到的那样,类型为非最终类的字段将导致声明类被认为是可变的,但这仍然是声明类的问题,而不是非最终类的问题不可变成员类。拥有不可变类的高层次结构是非常好的,其中所有非叶节点当然必须是非最终的。

    你应该检查一个字段是否是私有的;一个类拥有一个公共字段是完全可以的,并且该字段的可见性不会以任何方式、形状或形式影响声明类的不变性。您只需要检查该字段是否为final,其类型是否不可变。

    在检查一个类时,您首先要做的是递归确定其super 类的不变性。如果 super 是可变的,那么根据定义,后代也是可变的。

    那么,您只需要检查类的已声明字段,而不是所有字段。

    如果一个字段不是最终的,那么你的类是可变的。

    如果一个字段是最终的,但该字段的类型是可变的,那么你的类是可变的。 (数组根据定义是可变的。)

    如果一个字段是final,并且该字段的类型是Calculating,则忽略它并继续下一个字段。如果所有字段都是不可变的或Calculating,那么您的类是不可变的。

    如果字段的类型是接口、抽象类或非最终类,那么它被认为是可变的,因为您完全无法控制实际实现可能会做什么。这似乎是一个无法克服的问题,因为这意味着将可修改的集合包装在 UnmodifiableCollection 中仍然无法通过不变性测试,但实际上没问题,可以通过以下解决方法来处理。

    某些类可能包含非最终字段,但仍然实际上是不可变的String 类就是一个例子。其他属于这一类的类是包含非最终成员纯粹用于性能监控目的的类(调用计数器等),实现 popsicle immutability 的类(查找),以及包含成员是已知不会引起任何副作用的接口。此外,如果一个类包含真正的可变字段,但承诺在计算 hashCode() 和 equals() 时不考虑它们,那么该类在多线程方面当然是不安全的,但它仍然可以被视为为了将其用作地图中的键,它是不可变的。因此,所有这些情况都可以通过以下两种方式之一来处理:

    1. 手动将类(和接口)添加到不变性检测器。如果您知道某个类实际上是不可变的,尽管它的不变性测试失败了,您可以手动向检测器添加一个条目,将其与Immutable 关联。这样,检测器将永远不会尝试检查它是否是不可变的,它只会说“是的,它是”。

    2. 引入@ImmutabilityOverride 注释。您的不变性检测器可以检查字段上是否存在此注释,如果存在,它可能会将字段视为不可变,尽管该字段可能是非最终的或其类型可能是可变的。检测器还可以检查类上是否存在此注释,从而将类视为不可变的,甚至无需检查其字段。

    我希望这对后代有所帮助。

    【讨论】:

    【解决方案4】:

    我怀疑你可以通过单元测试来做到这一点。最好的方法是在编写课程或查看代码时要小心。正是因为对象上的方法可能会改变其状态的问题,而您可能从外部看不到。仅仅因为它被劝阻并不意味着它不会发生:-)

    【讨论】:

      【解决方案5】:

      很确定这是不可能的。考虑这个函数:

      public void doSomething() {
          if (System.currentTimeMillis() % 100000 == 0) {
              this.innerMember.changeState();
          }
      }
      

      首先,您无法通过运行每个类函数来检测它,因为该函数仅在 100 秒内精确地更改对象的状态一次。

      其次,你将无法通过解析代码来检测它,因为你不知道changeState()函数是否改变了innerMember的状态。

      【讨论】:

      • 你可以查看这个类的字节码和它使用的类。但是,恕我直言,这是付出了很多努力却没有太多收获。
      • @Peter Lawrey:不是真的,如果changeState() 使用反射来改变它的状态呢?没有包括这个案例以免事情过于复杂,但理论上是有可能的,你的字节码扫描方法不会找到它。
      • 你会拒绝任何使用反射作为不安全的类。数据值对象没有理由使用反射。
      【解决方案6】:

      这个帖子可以帮助How do I identify immutable objects in Java。看看第二个流行的答案,它可能会检查 FindBugs 的任何不变性问题。如果您在每次提交时都运行它,那么您可以将其称为单元测试:)

      编辑

      FindBugs 似乎只检查final,这并不多。您可以根据您在代码中使用的模式和类来实现自己的规则。

      【讨论】:

        最近更新 更多