15

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

  • (1) 它们是最终的和不可变的(尽管可能包含对可变对象的引用
  • (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

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

更新

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

...
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

让我们定义一个有趣的方法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;
}

这是一种奇怪的方法,我知道。但我想,它符合“任何计算或方法调用”的条件,不是吗?

4

5 回答 5

12

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

一旦b.get().add("a");被执行,a就不再是equalsto b,所以你没有理由期待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

于 2017-11-28T09:48:55.283 回答
9

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

如果程序试图区分对基于值的类的相等值的两个引用,无论是直接通过引用相等,还​​是通过诉诸同步、身份散列、序列化或任何其他身份敏感机制间接地,程序可能会产生不可预知的结果。在基于值的类的实例上使用这种对身份敏感的操作可能会产生不可预知的影响,应该避免。

(强调我的)

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

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

不过,我希望如果未来的 JVM 真正尝试替换基于值的实例,它必须排除引用可变对象的实例,以确保兼容性。如果您执行的操作产生 anOptional之后提取Optional, 例如… stream. findAny().get(),如果中间操作允许将元素替换为在中间使用点碰巧相等的另一个对象Optional(如果该元素是本身不是值类型)...</p>

于 2017-11-28T11:57:36.077 回答
7

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

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

很明显这a.equals(b)是真的。此外,因为Optional是最终的(不能被子类化),不可变的,并且两者都a引用b同一个列表,a.equals(b)始终为真。(嗯,几乎总是受到竞争条件的影响,即另一个线程正在修改列表,而这个线程正在比较它们。)因此,这似乎是 JVM 可以替代b的情况,a反之亦然.

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

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

如果程序试图区分对基于值的类的相等值的两个引用,无论是直接通过引用相等还是间接通过诉诸同步、身份散列、序列化或任何其他身份敏感机制,它都可能产生不可预知的结果。在基于值的类的实例上使用这种对身份敏感的操作可能会产生不可预知的影响,应该避免。

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

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

假设我们要bar通过某种方式(比如调试器)来检测方法,以便我们可以通过编程语言无法完成的方式检查参数值的属性,例如通过查看机器地址。即使a == b是真的(请记住,在值类型的世界中,==与 相同equals)我们也许能够确定这一点ab驻留在内存中的不同地址。

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

请注意,我们只能观察到这种差异,因为我们使用了一种额外的语言机制,例如调试器。在 Java 编程语言中,我们根本无法区分,因此这种替换不会影响任何 Java 程序的结果。这让 JVM 可以选择它认为合适的任何实现策略。JVM 可以自由地在堆上、栈上、每个上分配a一个b,作为不同的实例,或者作为相同的实例,只要 Java 程序无法区分。当 JVM 被授予实现选择的自由时,它可以使程序运行更快。

这就是第六个项目的要点。

于 2017-11-29T04:58:14.037 回答
2

当您执行这些行时:

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

在 assertEquals(a, b) 中,根据 API

  1. 将检查参数是否都是可选ab
  2. 物品都没有价值,或者,
  3. 当前值通过 equals() 彼此“相等”(在您的示例中,此等于是来自 ArrayList 的值)。

因此,当您更改 Optional 实例指向的 ArrayList 之一时,断言将在第三点失败。

于 2017-11-28T11:28:43.297 回答
1

第 6 点说如果 a & b 相等,那么它们可以互换使用,即如果一个方法需要 A 类的两个实例并且您已经创建了 a&b 实例,那么如果 a & b 通过第 6 点,您可以发送(a,a) or (b,b) or (a,b)所有三个将给出相同的输出.

于 2017-11-28T09:51:15.333 回答