基于价值的课程混乱

maa*_*nus 11 java immutability java-8 value-class

我正在寻求对基于价值的类定义的一些澄清.我无法想象,最后一个要点(6)应该如何与第一个一起工作

  • (1)它们是final和immutable(尽管可能包含对可变对象的引用)
  • (6)它们在相等时可自由替换,这意味着在任何计算或方法调用中根据equals()交换任意两个x和y实例应该不会产生明显的行为变化.

Optional 就是这样一堂课.

Optional a = Optional.of(new ArrayList<String>());
Optional b = Optional.of(new ArrayList<String>());
assertEquals(a, b); // passes as `equals` delegated to the lists

b.get().add("a");

// now bite the last bullet
assertTrue(a.get().isEmpty()); // passes
assertTrue(b.get().isEmpty()); // throws
Run Code Online (Sandbox Code Playgroud)

我读错了,还是需要更准确?

更新

伊兰的答案是有道理的(他们不再平等),但让我移动目标:

...
assertEquals(a, b); // now, they are still equal
assertEquals(m(a, b), m(a, a)); // this will throw
assertEquals(a, b); // now, they are equal, too
Run Code Online (Sandbox Code Playgroud)

让我们定义一个有趣的方法m,它会做一些变异并再次撤消它:

int m(Optional<ArrayList<String>> x, Optional<ArrayList<String>> y) {
    x.get().add("");
    int result = x.get().size() + y.get().size();
    x.get().remove(x.get().size() - 1);
    return result;
}
Run Code Online (Sandbox Code Playgroud)

我知道,这是一种奇怪的方法.但我想,它有资格作为"任何计算或方法调用",不是吗?

Era*_*ran 11

它们在相等时可自由替换,这意味着在任何计算或方法调用中根据equals()交换任意两个x和y实例应该不会产生明显的行为变化

一旦b.get().add("a");执行,a不再equalsb,所以你没有理由期望assertTrue(a.get().isEmpty());assertTrue(b.get().isEmpty());产生相同的结果.

基于值的类是不可变的这一事实并不意味着您不能改变存储在这些类的实例中的值(如中所述though may contain references to mutable objects).它只意味着一旦你创建了一个Optional实例Optional a = Optional.of(new ArrayList<String>()),你就不能改变a以保持对不同的引用ArrayList.


Hol*_*ger 8

您可以从您所指的规范中推导出您的操作的无效性:

如果程序试图将两个引用区分为基于值的类的相等值,无论是直接通过引用相等还是间接通过引发同步,身份哈希,序列化或任何其他身份敏感机制,程序可能会产生不可预测的结果.对基于值的类的实例使用此类身份敏感操作可能具有不可预测的影响,应该避免.

(强调我的)

修改对象一种身份敏感操作,因为它只影响具有您用于修改的引用所代表的特定标识的对象.

当您正在呼叫时,x.get().add("");您正在执行允许识别xy表示同一实例的操作,换句话说,您正在执行身份敏感操作.

尽管如此,我希望如果未来的JVM真正尝试替换基于值的实例,它必须排除引用可变对象的实例,以确保兼容性.如果你执行一个产生一个Optional后跟提取的操作Optional,例如… stream. findAny().get(),如果中间操作允许用另一个在中间Optional使用点碰巧相等的对象替换该元素,那将是灾难性的/不可接受的(如果该元素是本身不是一种价值类型)...

  • 当使用可变集合作为"HashMap"键时,您有类似的情况.它只有在地图中永远不会被修改时才有效,你总是要问自己,"我可以保证不变"...... (3认同)

Stu*_*rks 6

我认为一个更有趣的示例如下:

void foo() {
    List<String> list = new ArrayList<>();
    Optional<List<String>> a = Optional.of(list);
    Optional<List<String>> b = Optional.of(list);
    bar(a, b);
}
Run Code Online (Sandbox Code Playgroud)

显然,这a.equals(b)是真的。此外,由于Optional是最后的(不能被继承),一成不变的,都ab指向同一个列表中,a.equals(b)始终是真实的。(好吧,几乎总是受制于竞争条件,在该竞争条件下,另一个线程正在修改列表,而这个线程正在与它们进行比较。)因此,这似乎是一种情况,JVM可能会代替它ba反之亦然。

目前的情况(Java 8、9和10)而言,我们可以编写a == b,结果将是错误的。原因是我们知道这Optional是普通引用类型的实例,并且当前实现事物的方式Optional.of(x)将始终返回一个新实例,并且两个新实例永远不会==相互返回。

但是,基于值的类定义底部的段落表示:

如果程序试图直接通过引用相等性或通过呼吁同步,身份哈希,序列化或任何其他身份敏感机制间接地将两个引用区分为基于值的类的相等值,则可能会产生不可预测的结果。在基于值的类的实例上使用此类身份敏感的操作可能会产生不可预测的影响,应避免使用。

换句话说,“不要那样做”,或者至少不要依赖结果。原因是明天==操作的语义可能会改变。在假设的未来价值类型世界中,==可能会将其值类型重新定义为与相同equals,并且Optional可能会从基于值的类变为值类型。如果发生这种情况,a == b则将为true而不是false。

关于值类型的主要思想之一是它们没有身份的概念(或者Java程序可能无法检测到它们的身份)。在这样的世界里,我们怎么可能知道是否ab“真”是相同或不同?

假设我们将bar通过某种手段(例如调试器)来检测该方法,以便我们能够以编程语言无法实现的方式检查参数值的属性,例如查看机器地址。即使a == b为true(请记住,在值类型的世界==中与相同equals),我们也可以确定这一点a并将其b驻留在内存中的不同地址上。

现在假设JIT编译器编译foo并内联对的调用Optional.of。看到现在有两个代码块返回的两个结果始终为equals,编译器将消除其中一个代码块,然后在使用a或的地方使用相同的结果b。现在,在的测试版本中bar,我们可能会观察到两个参数值相同。由于第六个项目符号项目,允许JIT编译器执行此操作,该项目符号允许替换值equals

注意,我们只能观察到这种差异,因为我们使用的是诸如调试器之类的额外语言机制。在Java编程语言中,我们根本无法分辨出差异,因此这种替换不会影响任何Java程序的结果。这使JVM选择它认为合适的任何实现策略。JVM是可以自由分配的,a并且可以b在堆上,在堆栈上,在每个堆栈上分别作为不同的实例或相同的实例进行分配,只要Java程序无法区分它们即可。当JVM被授予的实现选择自由,它可以使程序走很多更快。

这就是第六项的要点。

  • 这就是我的理解。OP的问题是第六个项目符号在相等性方面定义了“ *可自由替换的*”,但是“ Optional”的相等性基于包含的对象,该对象可以是可变的,因此具有可变的相等性。正如我在回答中所说,我希望将来的优化能够像您描述的那样工作,能够折叠封装相同“ ArrayList”的值,但不能折叠两个“ Optional”封装两个不同但相等的“ ArrayList”实例的值。第六项说的话,这种方式很容易让人误解。 (3认同)
  • 即使对于不可变的对象,由于行为上的差异,尽管相等,但是Optional.of(Collections.emptyList())和Optional.of(List.of())仍不能被认为是可自由替换的,例如对于.filter(l-&gt; l.contains(null))`。目前,我想说的是,只有封装相同的对象实例或封装值类型时,它才能被自由替换。仅考虑平等是不够的。 (3认同)
  • @Holger这样:*仅当封装相同的对象实例或封装值类型时,才可以将其自由替换-应该添加到该文档中。优秀! (2认同)