【问题标题】:Do fields need to be explicitly final to have a "proper" immutable object?字段是否需要明确地是最终的才能拥有“正确的”不可变对象?
【发布时间】:2010-05-17 22:43:29
【问题描述】:

您经常读到不可变对象要求最终字段在 Java 中是不可变的。事实是这样吗,还是仅仅没有公共可变性而不实际改变状态就足够了?

例如,如果您有一个由构建器模式构建的不可变对象,您可以通过让构建器在构建时分配各个字段来做到这一点,或者让构建器自己保存字段并最终通过传递返回不可变对象其(私有)构造函数的值。

拥有 final 字段具有防止实现错误的明显优势(例如允许代码保留对构建器的引用并多次“构建”对象,同时实际上改变现有对象),但让构建器存储其构建时对象内部的数据似乎是 DRYer。

所以问题是:假设 Builder 不会及早泄漏 Object 并且一旦构建就停止修改对象(例如通过将其对对象的引用设置为 null)实际上是否有任何收获(例如提高线程安全性)如果将对象的字段设为最终字段,则对象的“不变性”?

【问题讨论】:

  • 如果我没记错的话,final 还可以帮助防止流氓程序员使用反射访问类的字段并修改其内容的“反射攻击”。有一个著名的例子完全颠倒了 String 类,并表明在很多情况下 String 实际上可以被修改(如果不是由于非最终性,而是由于事实上,一旦底层 char[] 被访问,如果“游戏结束”,因为你不能强制数组内容的不变性......但这不是我的意思:我的意思是反射可以帮助做讨厌的东西)。
  • @user,面对这样的反思,你无法建立不变性。然而,安全管理器的目的是让运行这样的第三方代码变得可行。
  • 等等...如果我有一个具有唯一 final intfinal 类,您可以使用反射来修改它吗?我的观点正是没有使用final你不能防止反射攻击(除非你有一个Security Manager,但它几乎从来不是案例,这就是为什么我写了“在很多情况下都可以修改”...)。但是,我不确定您是否可以使用反射来修改 final int ...你昨天的问题顺便说一句:)
  • @user988052,抱歉直到现在才看到您的评论。您可能无法设置原语(与将其编译为常量有关),但您可以更改对象引用 - 如果它不是静态的,则在 1.5 和 1.6 中。见javaspecialists.eu/archive/Issue096.html

标签: java immutability


【解决方案1】:

是的,您确实从 final 字段中获得了“线程安全”。也就是说,在构造过程中分配给final字段的值保证对所有线程都是可见的。线程安全的另一种选择是声明字段volatile,但是每次读取都会产生很高的开销……并且让任何查看您的类并想知道为什么这个“不可变”类的字段被标记为“易失性”的人感到困惑。”

标记字段final 在技术上是最正确的,并且最清楚地传达了您的意图。不幸的是,它确实使构建器模式非常麻烦。我认为应该可以创建一个注释处理器来为一个不可变类合成一个构建器,就像 Project Lombok 对 setter 和 getter 所做的那样。真正的工作将是所需的 IDE 支持,以便您可以针对实际上并不存在的构建器进行编码。

【讨论】:

  • @curiousguy: volatile 将确保一个线程写入的值对另一个线程可见。例如,如果没有它,读取器可能无法清除缓存在寄存器中的值并读取到主存储器。
  • "线程安全的另一种选择是声明字段volatile" 你能描述一下正确的方法吗?
  • @erickson - 具有既不是final 也不是volatile 的字段的类可能仍然是线程安全的,只要这些字段在构造期间完全初始化并且之后保持不变。构造完成后,内存中存在一个完全形成的对象,并且对该对象的任何引用都必须引用完全形成的对象。由于它不会改变,因此不可能看到陈旧的值。
  • @Scrubbie 有点像。这样的对象必须“安全地发布”。 (例如,将新对象分配给 volatile 变量,由其他线程读取。)仅仅避免字段突变是不够的。
【解决方案2】:

一个对象当然可以有可变的私有字段并且仍然作为一个不可变的对象工作。满足不变性合同的所有重要因素是对象从外部看起来是不可变的。例如,具有非最终私有字段但没有设置器的对象将满足此要求。

事实上,如果您的封装是正确的,那么您实际上可以改变内部状态,并且仍然可以作为“不可变”对象成功运行。一个例子可能是某种惰性求值或数据结构的缓存。

例如,Clojure 在其惰性序列的内部实现中就这样做了,这些对象表现得好像它们是不可变的,但只有在直接请求它们时才实际计算和存储未来值。任何后续请求都会检索存储的值。

但是 - 我要补充一点,您实际上想要改变不可变对象内部的地方的数量可能非常少。如果有疑问,请将它们定为最终版本。

【讨论】:

  • “具有非最终私有字段但没有设置器的对象将满足此要求” - 这本身并不能保证不变性。 Getter 可以返回数据成员引用,这些引用可以由调用者修改。
  • @Eyal: true - 尽管引用本身是不可变的。如果您使用不可变成员构造对象,那么不变性将一直保持下去。或者,在某些情况下,创建不可变组合或可变对象集合确实有意义。
  • 应该添加 - 不同的人对不变性的定义略有不同......我的是“从观察者的角度来看的不变性”,我认为这是最有用的,但我也听说人们将不变性定义为绝对不允许任何更改。
  • @mikera:我的首选定义是,如果一个对象是不可变的,则将任何特定字段的内容替换为曾经写入该字段的任何值,或者做同样,对于任何字段组合,不应影响其有效性或状态,但可能会影响性能。
【解决方案3】:

我认为您只需要考虑其运行的环境并确定使用反射来操作对象的框架是否存在危险。

人们可以很容易地编造一个奇怪的场景,其中一个被认为不可变的对象通过 POST 注入攻击被破坏,因为 Web 绑定框架被配置为使用反射而不是 bean setter。

【讨论】:

    【解决方案4】:

    您绝对可以拥有一个具有非最终字段的不可变对象。

    例如查看 java.lang.String 的 java 1.6 实现。

    【讨论】:

    • @user988052 不会破坏类完整性和程序安全性吗?
    • @TacticalCoder - 甚至可以通过反射修改最终字段。因此,您可以拥有“非不可变字符串”的论点根本不正确,除非有人也愿意说在 Java 中不可能拥有不可变对象。 String 类在设计上是不可变的。反射回避了课程的设计,实际上与原始问题的上下文无关,也与此海报的回应无关。
    • @TacticalCoder。为什么-1。你认为 String 是可变的吗?你见过它在 1.6 中的实现吗?与任何设计理念一样,您要么信任分包商,要么信任您自己,根据操作规范操作设备和子部件。如果您开始在这些规范之外进行操作(例如对不可变对象使用反射),最终结果可能类似于 Chernobyl -> en.wikipedia.org/wiki/Chernobyl_disaster
    【解决方案5】:

    评论: @埃里克森

    像这样:

    X 类 { volatile int i, j; } XY; //线程A: X x = 新 X; x.i = 1; x.j = 2; y = x; //线程B: 如果(y!= null){ a = y.i; b = y.j; }

    ?

    【讨论】:

    • 线程 B 不能保证看到 y 已被分配;即使y 可能在B 读取它之前被分配,根据墙上的时钟,没有内存屏障,所以B 可能从y 读取一个陈旧的值null。在这里,如果您从ij 中删除volatile,并将其放在y 上,B 将看到ijy 的“当前”值。这是因为写入 volatile 字段会将任何字段的写入刷新到主内存,而读取 volatile 变量会刷新主内存中的所有缓存值。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多