Java:调用hashCode()和equals()时自动抛出UnsupportedOperationException的干净方法?

Java: clean way to automatically throw UnsupportedOperationException when calling hashCode() and equals()?

我们有一个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()毫无意义的每个实现中,我们总是重复同样的事情:

1
2
3
4
5
6
7
8
9
@Override
public int hashCode() {
    throw new UnsupportedOperationException("contract violation: calling hashCode() on such an object makes no sense" );
}

@Override
public boolean equals( Object o ) {
    throw new UnsupportedOperationException("contract violation: calling equals() on such an object makes no sense" );
}

然而,这是容易出错的:我们可能会错误地忘记剪切/粘贴这个文件,这可能会导致用户滥用这些对象(例如,将它们放入默认Java集合中)。

或者,如果不能进行注释来创建这种行为,AOP会工作吗?

有趣的是,真正的问题是EDOCX1,0,EDCOX1,1的存在,在Java层次的顶部,这在一些情况下是没有意义的。但是我们该如何干净地处理这个问题呢?


我同意你对这是一个问题的评价,即hashCodeequals首先被定义为对象。长期以来,我一直认为,处理平等问题的方式应该与排序相同——一个接口说"我可以与X的一个实例进行比较",另一个接口说"我可以比较X的两个实例"。

另一方面,这真的给你带来了什么麻烦吗?人们是否曾试图在不该使用的地方使用equalshashCode?因为即使您可以让代码库中的每个类在不适当地调用这些方法时抛出一个异常,对于您正在使用的其他类(无论是来自JDK库还是第三方库)也不会如此。

我相信你可以用某种形式或其他形式的AOP来实现这一点,不管这是正常的注释处理还是其他什么——但是你有证据证明这个奖励是值得努力的吗?

另一种看待它的方法是:只有在扩展另一个已经覆盖了hashCodeequals的类的情况下,这才是正确的?否则,您可以使用对象的hashcode/equals方法的"equality=identity"性质,这仍然很有用。你有很多属于这一类的课程吗?您是否可以编写一个单元测试来通过反射查找所有此类类型,并检查当您调用hashCode/equals时这些类型是否引发异常?(如果它们有一个无参数的构造函数,或者有一个已经检查过的类型的手动列表,那么这可以是自动化的——如果有一个新类型不在"已知良好"列表中,那么单元测试可能会失败。)


我不明白你为什么认为"在某些情况下你不能满足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值。


为什么不让您的IDE(eclipse/netbeans/intellij)为您生成hashCode()equals()方法呢?他们在这方面做得很好。

当然,AOP会起作用,但它相当复杂。这意味着您将无法在几乎没有集合或实用程序的情况下使用这些对象。

另一个合乎逻辑的解决方案是只删除那些不工作的方法的实现,而实际上只保留Object中的实现。


至少有两个等价关系可以在Java或.NET中的所有对象之间定义:

  • 如果用对y的引用覆盖x不会改变x或y的任何成员的当前或将来的行为,则两个对象引用x和y完全等效。

  • 两个对象引用x和y具有相等的状态,如果在未持久化与标识相关的哈希函数返回的值的程序中,将对x的所有引用与对y的所有引用交换将使程序状态保持不变。

我有一个参考(X)指向一辆福特开拓者。我还有一个(Y指向一只暹罗猫。它们是等价的吗?不,他们不是,所以X.equals(Y)应该是假的。事实上,物体的类型彼此之间没有关系不是问题——如果有什么关系的话,这会使事情变得更容易(如果唯一能等同于一只暹罗猫的东西是另一只暹罗猫,那么事实上,Y不是SiameseCat意味着X.equals()不需要检查任何其他东西。

虽然对于特定对象是否应实现等价的第一或第二个定义可能存在一些争论,但值得注意的是,定义equals的任何对象,不论其状态的任何其他方面如何,都将不同对象报告为不相等(如果x.equals(x)不匹配x.equals(y),则at表示y的行为与x不同)。因此,如果一个与equals没有更好的关系,那么从object继承的默认定义是一个非常好和有意义的定义。

hashCode唯一可能遇到问题的情况是,当存储在哈希表中时,代码可能(不明智地)改变对象的某些方面。适当的补救方法是使hashCode不依赖于对象状态的任何可变方面。如果一个对象的状态除了它的类之外没有其他有意义的不可变方面,只需为该类编一个任意的数字,并让hashCode总是返回这个值。对于这样的对象,大型哈希表的性能将很差,但小型哈希代码的工作将很好。不能为一个类型定义一个好的散列代码的事实不应该阻止它在一个散列表中使用,其中包含十几个左右的项。