我们有一个OO代码库,在很多情况下,hashcode()和equals()根本不起作用,主要原因如下:
There is no way to extend an
instantiable class and add a value
component while preserving the equals
contract, unless you are willing to
forgo the benefits of object-oriented
abstraction.
这是Joshua Bloch的"有效Java"的引文,这里有一个伟大的阿蒂玛文章的主题:
http://www.artima.com/lejava/articles/equality.html
我们完全同意这一点,这不是问题所在。
问题是:在某些情况下,你不能满足equals()合同的要求,那么自动使hashcode()和equals()抛出一个不受支持的操作异常的一个干净方法是什么?
注释有用吗?我在考虑类似于@NotNull:每一个@NotNull合同违反都会自动抛出一个异常,除了用@NotNull注释参数/返回值之外,您没有其他事情可做。
这很方便,因为它是8个字符("@notnull"),而不是不断重复相同的验证/抛出异常代码。
在我所关心的情况下,在hashCode()/equals()毫无意义的每个实现中,我们总是重复同样的事情:
然而,这是容易出错的:我们可能会错误地忘记剪切/粘贴这个文件,这可能会导致用户滥用这些对象(例如,将它们放入默认Java集合中)。
或者,如果不能进行注释来创建这种行为,AOP会工作吗?
有趣的是,真正的问题是EDOCX1,0,EDCOX1,1的存在,在Java层次的顶部,这在一些情况下是没有意义的。但是我们该如何干净地处理这个问题呢?
- +1拒绝在不需要时实现哈希代码和equals,甚至通过引发异常来确保不能调用它们。这是对咒语的一个欢迎的改变,你经常听说你必须做的第一件事就是实现这两种方法(并花大量的心思使它们正常工作),即使大多数对象不需要这两种方法。
- 我对Eclipse在编写实现接口的新类时提供的自动生成方法有一个相关的不满,那就是它们都是生成到return null、return false、do nothing的。我希望默认为throw UnsupportedOperationException("TODO")。
- @我用Eclipse模板做的正是这样,所有生成的方法体都会抛出UnsupportedOperationException。
- @蒂洛:我不同意。这违反了最小惊喜原则。更具体地说,在处理Java框架和任何第三方代码时,可能会产生意想不到的副作用。默认情况下,对于类,.equals()与==相同,这意味着它检查两个变量是否指向同一引用。这样做是无害的。
- @我不同意。如果你想检查它们是否是同一个引用,你可以使用==(虽然我不需要经常这样做,但实际上没有什么害处),而不等于(),这将是非常有害的。因为您不知道equals()方法是否已被重写,所以依赖对象的默认equals()方法使用==而不是直接使用==您自己是非常有害的。
- @蒂洛:我已经更改了Eclipse模板来实现这一点。它还添加了当前日期,这样我就可以知道方法被取消了多长时间。
我同意你对这是一个问题的评价,即hashCode和equals首先被定义为对象。长期以来,我一直认为,处理平等问题的方式应该与排序相同——一个接口说"我可以与X的一个实例进行比较",另一个接口说"我可以比较X的两个实例"。
另一方面,这真的给你带来了什么麻烦吗?人们是否曾试图在不该使用的地方使用equals和hashCode?因为即使您可以让代码库中的每个类在不适当地调用这些方法时抛出一个异常,对于您正在使用的其他类(无论是来自JDK库还是第三方库)也不会如此。
我相信你可以用某种形式或其他形式的AOP来实现这一点,不管这是正常的注释处理还是其他什么——但是你有证据证明这个奖励是值得努力的吗?
另一种看待它的方法是:只有在扩展另一个已经覆盖了hashCode和equals的类的情况下,这才是正确的?否则,您可以使用对象的hashcode/equals方法的"equality=identity"性质,这仍然很有用。你有很多属于这一类的课程吗?您是否可以编写一个单元测试来通过反射查找所有此类类型,并检查当您调用hashCode/equals时这些类型是否引发异常?(如果它们有一个无参数的构造函数,或者有一个已经检查过的类型的手动列表,那么这可以是自动化的——如果有一个新类型不在"已知良好"列表中,那么单元测试可能会失败。)
- 同意AOP方法。
- +1表示希望为equals/hashcode提供单独的接口。这将阻止人们使用对象作为哈希键,而这些哈希键在那里不起作用(在这方面,不可变的接口也很好)。
- 嗨,乔恩,关于奖励/努力,这当然是一个关注点:但如果在注释出现时,我建议使用@notnull注释,可能也会这么说。只要有人写一次,我们就可以重用它。有趣的是,我使用的是自定义模板,并没有考虑简单地在默认情况下使hashcode()和equals()引发unsupportedOperationException,然后简单地更改equals()和hashcode()有意义的类的实现。
- 我认为空值倾向于更大的关注。我已经看到了很多由于零关注而导致的错误,但我记不得曾经试图散列一些不应该散列的东西。
我不明白你为什么认为"在某些情况下你不能满足equals()契约"?平等的含义是由类定义的。因此,使用对象的等号是完全有效的。如果不重写equals,则将每个实例定义为唯一的。
似乎有一种误解,认为equals是需要重写的方法之一,它必须检查所有字段。我会持相反观点——除非你对平等的定义不同,否则不要凌驾于平等之上。
我也不同意Artima的文章,特别是"陷阱3:用可变字段定义相等"。类基于可变字段定义其相等性是完全有效的。用户在使用集合时应该注意这一点。如果可变对象在其可变状态上定义了其相等性,那么在一个实例发生更改后,不要期望两个实例相等。
我认为投掷不受支持的动作违反了平等的冲刺。对象的等于状态:
The equals method for class Object
implements the most discriminating
possible equivalence relation on
objects; that is, for any non-null
reference values x and y, this method
returns true if and only if x and y
refer to the same object (x == y has
the value true).
因此,我应该能够调用equals,并根据对象的equals定义或重写的equals定义获得一个true或false值。
- +1:即使没有其他有用的方法,您仍然可以通过在Object中使用equals或hashCode实现来应用对象标识作为相等度量。
- @乔阿希姆:是的,但是如果对象相等对他的类没有意义,而且他根本不需要equals/hashcode,那么抛出unsupportedOperation就更有意义了。当代码的某些部分错误地调用这些方法时,这至少有助于查找错误。
- @郭:1,我不认为,我知道,这已经被证明了:)你不同意马丁·奥德斯基(斯卡拉的作者)和约书亚·布洛克的观点。好的。你有权发表自己的意见。已经证明,首先,您根本无法保证equals()的传递性约定。即使是来自so fame和c.l.j.p.fame的全能jon skeet也同意我的说法,即对象层次结构顶部存在equals()和hashcode(),这在这里是个问题。我所关心的正是Thilo所说的:在没有意义的地方,它应该抛出一个不受支持的操作异常。
- @syntaxt3rr0r:什么才是"证明"?显然,一个类可以以中断的方式定义hashCode和equals(例如,让这些函数返回随机值),但决不能阻止适当设计的类实现equals,以便与任何其他适当设计的类进行转换。至于它们是否属于Object,我假设如果X和Y可以在同一个集合中共存,就可以问X是否等同于Y和X是否能够轻松回答。
- @ SytRoTr3RR0R:EDCOX1、0和EDCOX1 1的一个更大的问题源于Java不区分用于封装身份的对象引用,那些只封装不可改变属性的身份,那些封装非共享可变状态的那些,那些封装了对IntAn的引用的那些。可变类型的CES,但不会将这些实例暴露于任何可能使它们发生变异的东西,以及那些封装可变状态和标识的东西。为了让两个藏品能明智地相互比较,他们必须知道…
- …其中的引用代表什么。
为什么不让您的IDE(eclipse/netbeans/intellij)为您生成hashCode()和equals()方法呢?他们在这方面做得很好。
当然,AOP会起作用,但它相当复杂。这意味着您将无法在几乎没有集合或实用程序的情况下使用这些对象。
另一个合乎逻辑的解决方案是只删除那些不工作的方法的实现,而实际上只保留Object中的实现。
- @博佐+1,这是赫马尔熊猫在他的评论中所说的,我没有考虑(甚至很难使用自定义模板)。我太专注于做"@notnull":)
至少有两个等价关系可以在Java或.NET中的所有对象之间定义:
我有一个参考(X)指向一辆福特开拓者。我还有一个(Y指向一只暹罗猫。它们是等价的吗?不,他们不是,所以X.equals(Y)应该是假的。事实上,物体的类型彼此之间没有关系不是问题——如果有什么关系的话,这会使事情变得更容易(如果唯一能等同于一只暹罗猫的东西是另一只暹罗猫,那么事实上,Y不是SiameseCat意味着X.equals()不需要检查任何其他东西。
虽然对于特定对象是否应实现等价的第一或第二个定义可能存在一些争论,但值得注意的是,定义equals的任何对象,不论其状态的任何其他方面如何,都将不同对象报告为不相等(如果x.equals(x)不匹配x.equals(y),则at表示y的行为与x不同)。因此,如果一个与equals没有更好的关系,那么从object继承的默认定义是一个非常好和有意义的定义。
hashCode唯一可能遇到问题的情况是,当存储在哈希表中时,代码可能(不明智地)改变对象的某些方面。适当的补救方法是使hashCode不依赖于对象状态的任何可变方面。如果一个对象的状态除了它的类之外没有其他有意义的不可变方面,只需为该类编一个任意的数字,并让hashCode总是返回这个值。对于这样的对象,大型哈希表的性能将很差,但小型哈希代码的工作将很好。不能为一个类型定义一个好的散列代码的事实不应该阻止它在一个散列表中使用,其中包含十几个左右的项。