Java 不可变对象和延迟初始化。

Java 不可变对象和延迟初始化。,java,Java,以上是我正在看的文章。不可变对象大大简化了程序,因为它们: 允许hashCode使用延迟初始化,并缓存其返回值 有谁能解释一下作者在上面想说什么 线路 如果我的类标记为final,并且它的实例变量是不可变的,那么我的类是不可变的吗 仍然不是final,反之亦然我的实例变量是final,类是normal 该行的意思是,由于对象是不可变的,因此hashCode只需计算一次。此外,在构造对象时不必计算它——只需在首次调用函数时计算它。如果从未使用对象的hashCode,则永远不会计算该对象。因此ha

以上是我正在看的文章。不可变对象大大简化了程序,因为它们:

允许hashCode使用延迟初始化,并缓存其返回值

  • 有谁能解释一下作者在上面想说什么 线路
  • 如果我的类标记为final,并且它的实例变量是不可变的,那么我的类是不可变的吗 仍然不是final,反之亦然
    我的实例变量是final
    类是normal

该行的意思是,由于对象是不可变的,因此
hashCode
只需计算一次。此外,在构造对象时不必计算它——只需在首次调用函数时计算它。如果从未使用对象的
hashCode
,则永远不会计算该对象。因此hashCode函数可以如下所示:

@Override public int hashCode(){
    synchronized (this) {
        if (!this.computedHashCode) {
            this.hashCode = expensiveComputation();
            this.computedHashCode = true;
        }
    }
    return this.hashCode;
}

并补充其他答案

无法更改不可变对象。final关键字适用于基本数据类型,如int。但对于自定义对象,它并不意味着-它必须在您的实现中内部完成:

以下代码将导致编译错误,因为您正试图更改指向对象的最终引用/指针

final MyClass m = new MyClass();
m = new MyClass();
但是,这段代码可以工作

final MyClass m = new MyClass();
m.changeX();

如果对象是不可变的,它就不能改变它的状态,因此它的哈希代码也不能改变。这允许您在需要时计算值,并缓存值,因为它将始终保持不变。事实上,基于可变状态实现自己的
hasCode
函数是一个非常糟糕的主意,因为例如
HashMap
假设哈希不能更改,如果它确实更改,它将中断

延迟初始化的好处是hashcode计算会延迟到需要时。许多对象根本不需要它,所以可以保存一些计算。特别昂贵的散列计算,如long
String
s,从中受益匪浅

class FinalObject {
    private final int a, b;
    public FinalObject(int value1, int value2) {
        a = value1;
        b = value2;
    }

    // not calculated at the beginning - lazy once required
    private int hashCode;
    @Override
    public int hashCode() {
        int h = hashCode; // read
        if (h == 0) {
            h = a + b;    // calculation
            hashCode = h; // write
        }
        return h;         // return local variable instead of second read
    }
}
编辑:正如@assylias所指出的,只有在只有1次读取
hashCode
时,才能保证使用非同步/非易失性代码,因为该字段的每次连续读取都可能返回0,即使第一次读取可能已经看到不同的值。上面的版本修复了这个问题

Edit2:替换为更明显的版本,代码略少,但字节码大致相同

public int hashCode() {
    int h = hashCode; // only read
    return h != 0 ? h : (hashCode = a + b);
    //                   ^- just a (racy) write to hashCode, no read
}

正如其他人所解释的,因为对象的状态不会改变,所以hashcode只能计算一次

简单的解决方案是在构造函数中预先计算它,并将结果放入最终变量(这保证了线程安全)

如果您想进行延迟计算(hashcode仅在需要时计算),那么如果您想保持不可变对象的线程安全特性,那么这就有点棘手了

最简单的方法是声明
私有volatile int散列
并运行计算(如果为0)。除了hashcode实际为0的对象(如果hash方法分布良好,则为40亿分之一)之外,您将获得惰性

或者,您可以将它与一个易失性布尔值耦合,但需要注意更新这两个变量的顺序


最后,为了提高性能,您可以使用String类使用的方法,该方法使用额外的局部变量进行计算,从而在保证正确性的同时去掉volatile关键字。如果您不完全理解为什么要这样做,那么最后一个方法很容易出错…

除非您采取其他预防措施,否则您的实现不是线程安全的(一个线程可以观察到布尔值为true,但哈希代码的默认值为0)。@assylias布尔值仅在计算后设置为true,所以这不会发生,是吗?(虽然它可以再次调用
expensiveComputation
)。如果没有适当的同步,在多线程环境中可能会发生任何事情,包括指令重新排序……当哈希代码的计算结果为0时会怎么样?如果可以,最好使用
Integer
null
来表示未初始化,或者类似的东西。顺便说一句
String
可以做到这一点。@Dukeling创建对象的缺点是,它会分配一个对象并创建垃圾,否则就不会有垃圾。这就破坏了试图减少程序所做工作的目的。@PeterLawrey,除了这个版本不太安全之外。@Dukeling我更喜欢使用
long
而不是
Integer
并使用
int
不可能的值来取消设置。谢谢,你能解释一下第二点和另外一个问题吗。。不可变对象是否没有多个引用。。。这使得JVM到GC更容易使用。@Kevin第二点:将类标记为final并不能使其不可变,只是不能扩展。@Kevin不确定您的意思,但不可变对象可以包含多个引用,并且可以被多个其他对象引用。当然,只有当(从任何线程)可访问对象没有更多引用时,GC才会发生。您不需要线程安全。如果同时进行计算,则会产生相同的值,因为哈希代码必须是确定性的。如果你不保护它,并且很少计算两次,那也没关系。不安全=更快。@zapl它比这更复杂-Igor的实现可能会导致哈希代码被计算多次,正如您所指出的-但更重要的是,它也可能返回null。Java内存模型的一位作者详细介绍了为什么会发生这种情况: