在equals()方法中分配字段

时间:2014-12-21 03:22:22

标签: java equals lazy-initialization

假设您已经编写了一个类并且使用了延迟初始化来分配其中一个字段。假设该字段的计算仅涉及其他字段,并且保证每次都产生相同的结果。当该类的两个相等实例彼此相遇时,它们有意义地共享延迟初始化字段的值(如果其中任何一个知道它)。您可以使用equals()方法执行此操作。这是一个显示我的意思的课程。

final class MyClass {

    private final int number;
    private String string;

    MyClass(int number) {
        this.number = number;
    }

    String getString() {
        if (string == null) {
            string = OtherClass.expensiveCalculation(number);
        }
        return string;
    }

    @Override
    public boolean equals(Object object) {
        if (object == this) { return true; }
        if (!(object instanceof MyClass)) { return false; }
        MyClass that = (MyClass) object;
        if (that.number != number) { return false; }
        String thatString = that.string;
        if (string == null && thatString != null) {
            string = thatString;
        } else if (thatString == null && string != null) {
            that.string = string;
        }
        return true;
    }

    @Override
    public int hashCode() { return number; }
}

对我来说,如果你要去懒洋洋地初始化一个字段,那么这个信息共享似乎是合乎逻辑的事情,但我从来没有见过任何人使用equals()方法的例子。方式。

这是一种常见的还是标准的技术?如果是这样,它叫什么?如果这不是一种常用的技术,我可以问(有可能将这个问题暂时搁置为基于观点的风险)人们怎么看?除了检查相等性之外,使用equals()方法做一些事情是个好主意吗?

4 个答案:

答案 0 :(得分:3)

这对我来说很危险:使用Object的公共方法的副作用来设置对象的状态。如果你继承这个类,那么这会破坏,然后覆盖子类的equals方法,这是常见的事情。只是不要这样做。

答案 1 :(得分:2)

“假设该字段的计算仅涉及其他字段,并且保证每次都产生相同的结果。”

鉴于此假设,您可以断言延迟初始化字段的值无关紧要,因为如果其他字段的值相同,则计算的值也将相同。 / p>

修改 我想我回避了原来的问题,所以我也会回答这个问题。在您创建的场景中,您提议的内容没有任何内在错误。

我要提出的论点只是从务实的角度出发:当其他人改变getString()的定义时(或更可能 - 改变导致该值的长时间运行计算的定义)会发生什么?它开始依赖于不属于对象平等考虑因素的东西?

传统观点认为equals()应该是无副作用的原因是大多数开发人员都认为它是无副作用的。

答案 2 :(得分:2)

我不会这样做,原因有三:

  1. 一般的软件工程原则,例如凝聚力,松散耦合,以及不重复自己",不利于它:你的equals(...)方法会做某事非常"等于" -y,与您的getString()方法的逻辑重叠。更新getString()逻辑的人可能无法注意到他们是否还需要更新equals(...)的逻辑。 (您可能会认为equals(...)的逻辑将继续是正确的,无论 getString()如何更改 - 毕竟,您只是equals(...)将引用从一个对象复制到一个对象,所以大概应该始终保持不变? - 但问题是复杂的系统以你无法提前预测的方式发展。当需求发生变化时,你不希望对代码中与要求明显相关的部分内容进行随机更改。)

  2. 线程安全性。您的string字段目前不是volatile,而您的getString()方法目前不是synchronized,因此没有尝试过线程 - 无论如何安全在这里;但是如果你要让类的其余部分保持线程安全,那么将equals(...)更改为线程安全而不会有死锁的风险就不是那么简单了。 (这与第1点重叠,但我单独列出它,因为#1完全是因为知道你必须更改equals(...)的难度,而这个问题即使给予知识也很难解决。)

  3. 不太可能有用。没有太多理由期望它经常发生两个实例得到equals(...) - 比较一个已经懒惰初始化而另一个没有;所以额外的代码复杂性和上面提到的缺点不太可能是值得的。 (请记住:代码不是免费的。为了通过成本效益分析,一段代码的好处必须超过将来测试,理解,维护和支持它的成本。)如果它是值得的要在等效实例之间共享这些延迟初始化的值,那么这应该以更清晰,更有条理的方式完成,而不依赖于偶然事件。 (例如,您可以将类的构造函数设为私有,并使用static工厂方法在创建和返回新实例之前检查static WeakHashMap是否存在现有实例之一。)

答案 3 :(得分:1)

您描述的方法有时是一个很好的方法,特别是在很多大型不可变对象尽管是独立构造的情况下最终可能完全相同的情况下。因为比较相等的引用比比较恰好相同的大对象要快得多,所以使用比较两个大对象并发现它们相同的代码替换其中一个引用并引用另一个引用可能是有利的。 。为了使其可行,应该尝试在所讨论的对象之间建立某种排序,以确保重复比较最终将产生相同的规范值。这可以通过让对象包含long序列号并通过引用较旧但相等的值一致地替换对较新值的引用,或者通过比较相等引用的identityHashCode值并丢弃其中任何一个来实现。一个(如果有的话)具有较低的值(如果两个引用标识不同但相同的实例,恰好报告相同的identityHashCode,则两者都应保留)。

令人讨厌但不幸的是,Java对有效不可变的对象的多线程支持非常差。要使有效不可变对象具有线程安全性,对数组或非final字段的任何访问都必须通过final字段。实现这一目标的最便宜的方法可能是让对象包含一个final字段,在该字段中存储对自身的引用,并且所有访问非final字段的方法都通过{{1}来实现。字段,但那有点难看。尽管存在愚蠢的冗余final字段访问(因为最终字段的目标将保证在缓存中,因此,更改引用不同但相同的引用与对同一对象的引用可以提供一些显着的性能优势,解除引用它将比正常的解除引用便宜得多。)

顺便说一句,在许多情况下,它可能包含一个"等价关系"这样的机制,一旦某些对象被比较并发现它们是相等的,发现它们中的任何一个等于另一个对象将导致所有这些对象都可以快速识别。然而,我还没有想出如何避免故意 - 但合法的使用模式导致内存泄漏的可能性。