【问题标题】:immutable objects and lazy initialization.不可变对象和延迟初始化。
【发布时间】:2013-09-22 21:35:32
【问题描述】:

http://www.javapractices.com/topic/TopicAction.do?Id=29

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

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

  • 谁能解释一下作者在上面的意思 行。
  • 是我的类immutable,如果它被标记为final并且它的实例变量 仍然不是最终的和vice-versa my instance variables being finalclass being normal

【问题讨论】:

    标签: java


    【解决方案1】:

    正如其他人解释的那样,因为对象的状态不会改变哈希码只能计算一次。

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

    如果您想进行惰性计算(仅在需要时计算哈希码),如果您想保持不可变对象的线程安全特性,那就有点棘手了。

    最简单的方法是声明一个private volatile int hash;,如果它为 0,则运行计算。除了哈希码确实为 0 的对象(如果您的哈希方法分布良好,则为 40 亿分之一)之外,您将变得懒惰。

    或者,您可以将其与 volatile 布尔值耦合,但需要注意更新两个变量的顺序。

    最后,为了获得额外的性能,您可以使用 String 类使用的方法,该方法使用额外的局部变量进行计算,从而在保证正确性的同时摆脱 volatile 关键字。如果您不完全理解为什么要以这种方式完成,那么最后一种方法很容易出错......

    【讨论】:

    • 谢谢,你能解释一下第二点和另一个问题吗?不可变对象没有多个引用吗?这让 JVM 到 GC 更容易。
    • @Kevin 第二点:将类标记为 final 并不能使其不可变,只是不可扩展。
    • @Kevin 不确定您的意思,但不可变对象可以包含多个引用,并且可以被多个其他对象引用。当然,只有在(来自任何线程)可访问对象没有更多引用时才会发生 GC。
    • 你不需要线程安全。如果计算同时发生,它将产生相同的值,因为 hashCode 必须是确定性的。如果你不保护它并且很少计算它两次也没关系。不安全 = 更快。
    • @zapl 它比这更复杂 - 正如你所指出的,Igor 的实现可能导致哈希码被计算多次 - 但更重要的是,它也可能返回 null。更多关于它为什么会发生的信息,作者是 Java 内存模型的作者之一:jeremymanson.blogspot.co.uk/2008/12/…
    【解决方案2】:

    如果你的对象是不可变的,它就不能改变它的状态,因此它的哈希码也不能改变。这使您可以在需要时计算该值并缓存该值,因为它始终保持不变。实际上,基于可变状态实现自己的 hasCode 函数是一个非常糟糕的主意,因为例如HashMap 假设哈希不能改变,如果改变就会破坏。

    延迟初始化的好处是哈希码计算延迟到需要时。许多对象根本不需要它,因此您可以节省一些计算。特别是昂贵的哈希计算,如 long Strings 会从中受益。

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

    编辑:正如@a​​ssylias 所指出的,只有在只有一次读取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
    }
    

    【讨论】:

    • 当哈希码计算为 0 时呢?如果可以的话,最好使用Integernull 来表示未初始化或类似的东西。
    • 顺便说一句 String 这样做。
    • @Dukeling 创建对象的缺点是它会分配一个对象并创建否则不会存在的垃圾。这违背了试图减少程序所做工作的意义。
    • @PeterLawrey 除了这个版本不是那么安全。
    • @Dukeling 我宁愿使用 long 而不是使用 Integer 并使用 int 无法设置的值。
    【解决方案3】:

    该行的意思是,由于对象是不可变的,因此 hashCode 只需计算一次。此外,它不必在构造对象时计算 - 只需在第一次调用函数时计算。如果对象的hashCode 从未使用过,则它永远不会被计算。所以 hashCode 函数可能看起来像这样:

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

    【讨论】:

    • 除非您采取额外的预防措施,否则您的实现不是线程安全的(线程可以观察到布尔值的值为 true,但哈希码的默认值为 0)。
    • @assylias 布尔值仅在计算 之后设置为 true,所以这不会发生,对吗? (虽然它可以再次调用expensiveComputation。)
    • 如果没有适当的同步,多线程环境中可能会发生任何事情,包括指令重新排序...
    【解决方案4】:

    并添加到其他答案。

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

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

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

    但是这段代码可以工作。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-05-10
      • 1970-01-01
      • 2011-11-17
      • 1970-01-01
      • 2019-07-15
      • 1970-01-01
      相关资源
      最近更新 更多