Java 在equals()方法中指定字段

Java 在equals()方法中指定字段,java,equals,lazy-initialization,Java,Equals,Lazy Initialization,假设您编写了一个类,并使用惰性初始化来分配它的一个字段。假设该字段的计算只涉及其他字段,并且保证每次都产生相同的结果。当类的两个相等实例彼此相遇时,如果其中一个知道延迟初始化字段的值,则它们共享该值是有意义的。您可以使用equals方法来实现这一点。这里有一节课说明了我的意思 final class MyClass { private final int number; private String string; MyClass(int number) {

假设您编写了一个类,并使用惰性初始化来分配它的一个字段。假设该字段的计算只涉及其他字段,并且保证每次都产生相同的结果。当类的两个相等实例彼此相遇时,如果其中一个知道延迟初始化字段的值,则它们共享该值是有意义的。您可以使用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方法除了检查相等性之外,还能做其他事情吗?

这对我来说很危险:使用对象的公共方法的副作用来设置对象的状态。如果您将该类子类化,然后重写该子类的equals方法,这将中断,这是一件常见的事情。不要这样做。

这对我来说很危险:使用对象的公共方法的副作用来设置对象的状态。如果您将该类子类化,然后重写该子类的equals方法,这将中断,这是一件常见的事情。不要这样做。

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

根据此假设,您可以断言延迟初始化字段的值并不重要,因为如果其他字段的值相同,则计算值也将相同

编辑 我想我避开了原来的问题,所以我也会回答这个问题。在您创建的场景中,您的提议没有本质上的错误

我要提出的论点仅仅是从实用的角度出发:当其他人正在更改getString的定义或更可能更改长时间运行的计算的定义时,会发生什么情况?该计算会导致该值,并且它开始依赖一些不属于对象平等性考虑的内容


传统智慧认为equals应该没有副作用的原因是大多数开发人员希望它没有副作用。

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

根据此假设,您可以断言延迟初始化字段的值并不重要,因为如果其他字段的值相同,则计算值也将相同

编辑 我想我避开了原来的问题,所以我也会回答这个问题。在您创建的场景中,您的提议没有本质上的错误

我要提出的论点仅仅是从实用的角度出发:当其他人正在更改getString的定义或更可能更改长时间运行的计算的定义时,会发生什么情况?该计算会导致该值,并且它开始依赖一些不属于对象平等性考虑的内容


传统智慧认为equals应该没有副作用的原因是大多数开发人员希望它没有副作用。

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

一般的软件工程原则,如内聚、松耦合和不要重复自己,都会对它产生不利影响:你的平等。。。方法将执行一些与getString方法的逻辑重叠的不太相等的操作。更新getString逻辑的人可能没有注意到是否还需要更新equals的逻辑。。。。你可能会认为平等的逻辑。。。无论getString如何更改,都将继续保持正确-毕竟,您只是拥有了equals。。。将引用从一个对象复制到一个等效的对象,因此假定该对象应始终保持不变但问题是,复杂系统的发展方式往往无法事先预测。当需求发生变化时,您不希望对代码中与需求不明显相关的部分进行随机更改

线程安全。您的字符串字段当前不是易失性的,并且您的getString方法当前没有同步,因此这里没有线程安全的尝试;但是,如果您要使类的其余部分线程安全,那么更改equals并不是非常简单的。。。确保线程安全,不会出现死锁风险。这与第1点有点重叠,但我将其单独列出,因为第1点仅仅是关于知道必须更改等于的难度,而这个问题即使在已知的情况下也有点棘手 e

不太可能有用。没有太多的理由期望两个实例得到相等的结果会经常发生…-当一个已经被延迟初始化,而另一个还没有初始化时进行比较;因此,额外的代码复杂性和上面提到的缺点可能不值得。记住:代码不是免费的。为了通过成本效益分析,一段代码的效益必须超过未来测试、理解、维护和支持它的成本。如果在等价实例之间共享这些惰性初始化值是值得的,那么应该以更清晰、更有组织的方式进行,而不依赖偶然事件。例如,您可以将类的构造函数设为私有,并使用静态工厂方法在创建和返回新实例之前检查静态WeakHashMap是否存在现有实例


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

一般的软件工程原则,如内聚、松耦合和不要重复自己,都会对它产生不利影响:你的平等。。。方法将执行一些与getString方法的逻辑重叠的不太相等的操作。更新getString逻辑的人可能没有注意到是否还需要更新equals的逻辑。。。。你可能会认为平等的逻辑。。。无论getString如何更改,都将继续保持正确-毕竟,您只是拥有了equals。。。将引用从一个对象复制到一个等效的对象,因此假定该对象应始终保持不变但问题是,复杂系统的发展方式往往无法事先预测。当需求发生变化时,您不希望对代码中与需求不明显相关的部分进行随机更改

线程安全。您的字符串字段当前不是易失性的,并且您的getString方法当前没有同步,因此这里没有线程安全的尝试;但是,如果您要使类的其余部分线程安全,那么更改equals并不是非常简单的。。。确保线程安全,不会出现死锁风险。这与第1点有点重叠,但我单独列出它,因为第1点仅仅是关于知道必须更改等于的困难,而这个问题即使在已知的情况下也有点棘手

不太可能有用。没有太多的理由期望两个实例得到相等的结果会经常发生…-当一个已经被延迟初始化,而另一个还没有初始化时进行比较;因此,额外的代码复杂性和上面提到的缺点可能不值得。记住:代码不是免费的。为了通过成本效益分析,一段代码的效益必须超过未来测试、理解、维护和支持它的成本。如果在等价实例之间共享这些惰性初始化值是值得的,那么应该以更清晰、更有组织的方式进行,而不依赖偶然事件。例如,您可以将类的构造函数设为私有,并使用静态工厂方法在创建和返回新实例之前检查静态WeakHashMap是否存在现有实例


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

一个令人讨厌但不幸的缺点是Java对有效不可变对象的多线程支持非常差。要使有效的不可变对象是线程安全的,对数组或非final字段的任何访问都必须经过final字段。实现这一点最便宜的方法可能是让对象包含一个final字段,它在其中存储对自身的引用,并让所有访问非final字段的方法都通过该final字段进行访问,但这有点难看。尽管存在一些愚蠢的错误,但是使用对同一对象的引用更改不同但相同的引用仍然可以提供一些显著的性能优势 冗余的最终字段访问因为最终字段的目标将被保证在缓存中,所以取消对它的引用将比正常的取消引用便宜得多


顺便说一句,在许多情况下,可能会包括一种等价关系机制,这样,一旦对一些对象进行比较并发现它们相等,发现其中任何一个对象等于另一个对象,就会使所有对象都很快被识别出来。但是,我还没有弄清楚如何避免故意使用令人讨厌但合法的使用模式导致内存泄漏的可能性。

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

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


顺便说一句,在许多情况下,可能会包括一种等价关系机制,这样,一旦对一些对象进行比较并发现它们相等,发现其中任何一个对象等于另一个对象,就会使所有对象都很快被识别出来。然而,我还没有弄清楚如何避免一种故意恶意但合法的使用模式导致内存泄漏的可能性。

但这真的是一种副作用吗?您不会更改任何会影响从外部查看实例的方式的内容。该类实际上是不可变的。另外,您不能重写equals-我故意将类设置为final,因为这个原因。@pbabcdefp您没有更改任何会影响从外部查看实例的内容。那么,更改字符串的值不会影响getString的返回值吗?@Tom非常非常仔细地阅读了代码。您正在设置字符串,但是getString方法无论如何都不会返回null的原始值。但是这真的是一个副作用吗?您不会更改任何会影响从外部查看实例的方式的内容。该类实际上是不可变的。另外,您不能重写equals-我故意将类设置为final,因为这个原因。@pbabcdefp您没有更改任何会影响从外部查看实例的内容。那么,更改字符串的值不会影响getString的返回值吗?@Tom非常非常仔细地阅读了代码。您正在设置字符串,但getString方法无论如何都不会返回null的原始值。只要expensiveComputation每次返回相同的结果,equals方法基本上没有副作用。我同意在另一个类中定义ExpensiveComputing并不好,但这是为了使我的示例尽可能简单。如果我真的这么做了,大家都可以放松一下——我从来没有这样做过,我会把计算字符串的代码放在MyClass中,并附上一条注释,指出对代码的任何更改都不能改变结果应该只取决于数字的值这一事实。我知道你是从哪里来的,但问题不在于是否有可能使缓存操作变得安全,而缓存操作本质上就是您在这里模拟的操作。这是一个以这样的方式写它需要花费多少时间和精力的问题,然后确保没有人改变它。在这一点上,我要说的是,对于j
证明不必运行两次所需的工作量。这是反对惰性初始化的理由,惰性初始化是一种非常常见的技术。一些涉及延迟初始化的标准习惯用法与此一样复杂。java的双校验成语在脑海中浮现。我认为,在等值内分配值所节省的工作量在我认为它是正当的之前,必须是非常重要的,因为默认的期望所带来的权重等于没有副作用。模型没有提供合理的方式来表达这个概念。如果代码不会因为看到或不看到初始化而感到麻烦,那么懒洋洋地初始化一个原语,或者对一个新实例的引用——一个在内存模型下被限定为不可变的类——将是安全的。但是,延迟初始化对新创建的可变类型实例(如数组)的引用是不安全的,即使数组在公开后永远不会被修改[我知道JVM实现不应该有任何硬件平台……只要ExpensiveComputing每次都返回相同的结果,equals方法基本上没有副作用。我同意ExpensiveComputing在另一个类中定义并不好,但这是为了使我的示例尽可能简单。如果我真的这样做的话这一点大家都可以放松了——我从来没有这样做过,我会把计算字符串的代码放在MyClass中,并附上一条注释,指出对代码的任何更改都不能改变这样一个事实,即结果应该只取决于数字的值。我理解您来自何方,但这不是是否可以创建缓存的问题操作基本上就是你在这里模仿的安全操作。问题是这样写需要花费多少时间和精力,然后确保没有人改变它。在这一点上,我要说的是,为了证明不需要运行它所需的工作量是合理的,计算成本必须非常昂贵两次。这是一个反对懒惰初始化的论点,这是一种非常常见的技巧。一些涉及懒惰初始化的标准习语也和这一样复杂。双校验习惯用语会浮现在脑海中。我认为,在考虑之前,在等式中分配值所节省的工作量必须是惊人的。这是合理的,因为默认期望equals的权重没有副作用。Java中延迟初始化的一个危险是它的内存模型没有提供合理的方式来表达这个概念。延迟初始化一个原语,或者对一个新实例的引用,一个在m如果代码不会因为看到或不看到初始化而烦恼,那么emory模型是安全的。但是,懒洋洋地初始化对新创建的可变类型实例(如数组)的引用是不安全的,即使数组在公开后永远不会被修改[据我所知,JVM实现不应该有任何硬件平台……如果这真的能让您的应用程序在使用真实的生产数据(如生产数据)进行性能测试时在很大程度上表现得更好,那么就去做吧。但是,要小心记录好这一点,最好是在代码中,并确保单元测试在各个方面都涵盖了这一行为;否则,在几个月后必须维护代码或自己的下一个人将痛恨你。如果这真的让你的应用程序在使用真实的生产数据(如生产数据)进行性能测试时表现得更好,那么这也是生产测试但是要小心,最好是在代码中记录好这一点,并确保有单元测试从各个方面涵盖了这一行为;否则,在几个月后你或你自己必须维护代码的下一个人会对你恨之入骨。谢谢你的回答。我同意第1点和第3点-代码是不可维护的,WeakHashMap显然是通往缓存的明智之路。关于第2点,我不太确定。我只是在Java并发性实践的第3页上,所以我要问的可能暴露了完全的无知……但是如果变量字符串不是易失的,为什么重要呢?当然这意味着另一个线程可能看不到,可能会导致以后不必要地执行昂贵的计算。但这并不意味着该类不是线程安全的,或者是吗?@pbabcdefp:Hmm,好问题。规范不允许感知到完全虚假的写入,所以如果线程1将字符串从null更新为非null引用,那么read2只能看到null或该引用;String类在内部使用final来获得特殊保证,以确保任何观察到非null的线程
String类型的引用将观察完全初始化的String对象。参见JLS第17.5节。我写这段话的前提是你不想让expensiveCalculation被调用多次[continued][continued],但是如果你真的对这种重复调用没问题,那么是的,我认为现有的代码是线程安全的。但我还是坚持这一段。如果您决定想要一个更强大的线程安全保证,或者如果您将该字段更改为字符串以外的内容,因此必须使用更明确的线程安全机制,或者诸如此类的话,那么保持相等将有点棘手。。。你完全正确,我真的不想让多个电话来花费计算。在我的示例中,我没有使用volatile和/或synchronized的唯一原因是我对如何正确使用它们没有信心。@pbabcdefp:在equals中没有此逻辑的情况下,您可以只将getString标记为synchronized,或者将string标记为volatile,并在getString:public string getString中使用双重检查锁定{if string==null{synchronized此{if string==null{string=OtherClass.expensiveCalculationnumber;}}}}}返回字符串;}。前一种方法更简单,如果这些对象很少在线程之间共享,那么性能可能会更好;后一种方法如果这些对象经常在线程之间共享,那么通过减少锁定,性能会更好。谢谢你的回答。我同意第1点和第3点-代码是不可维护的,而WeakHashMap显然是最重要的关于第2点,我不太确定。我只是在Java并发的第3页上,所以我要问的可能暴露了完全的无知……但是如果变量字符串不是易变的,为什么重要呢?当然,这意味着可能看不到另一个线程分配给它的值,可能会导致expensiveCalcula但这并不意味着该类不是线程安全的,或者是吗?@pbabcdefp:Hmm,好问题。规范不允许感知到完全虚假的写入,因此如果线程1将字符串从null更新为非null引用,那么线程2只能看到null或该引用;而字符串class在内部使用final来获得特殊保证,以确保任何观察到String类型的非空引用的线程都会观察到完全初始化的String对象。请参阅JLS的第17.5节。我在编写该段时假设您不希望将expensiveCalculation称为multiple[续][续]好几次了,但是如果你真的同意这样的重复调用,那么是的,我认为现有的代码是线程安全的。但无论如何,我支持这一段。如果你决定想要更强大的线程安全保证,或者如果你将该字段改为字符串以外的内容,因此必须使用更明确的线程安全机制,或者诸如此类的,那么它就可以了在不冒死锁风险的情况下保持相等会有点棘手。你完全正确,我真的不希望多次调用来花费计算。在我的示例中,我没有使用volatile和/或synchronized的唯一原因是我对如何正确使用它们没有信心。@pbabcdefp:例如,您可以只将getString标记为已同步,或者将string标记为易失性,并在getString:public string getString{if string==null{synchronizedthis{if string==null{string=OtherClass.expensiveCalculationnumber;}}}}}返回字符串;}。前一种方法更简单,如果这些对象很少在线程之间共享,则性能可能会更好;后一种方法通过减少锁定,如果这些对象经常在线程之间共享,则性能会更好。