在升级了一个内部自定义依赖项(我们公司开发的库)之后,我在 Java 应用程序(Spring 批处理作业)中面临着一个奇怪的结果。 在代码升级后,两个相同类型的新不同对象显示具有相同的哈希代码。
CustomObject oj1 = new CustomObject();
oj1.setId(1234L);
CustomObject oj2 = new CustomObject();
oj2.setId(9999L);
System.out.println(oj1); //Prints CustomObject@1
System.out.println(oj2); //Prints CustomObject@1
System.out.println(oj1.hashCode()); //Prints 1
System.out.println(oj2.hashCode()); //Prints 1
在意识到具有 HashSet 变量的单元测试之一仅添加第一个对象而忽略其余对象后,我注意到了这个问题。显然,hashSet 正在做应该做的事情,但对象不应该是相同的,并且是具有不同 ID 的新实例。我在应用程序中的单元测试之外测试了同样的东西,但仍然是相同的问题。一旦我恢复到旧的依赖代码行为正常,上面的打印语句显示不同的数字!我确定其中一个依赖项导致了此问题,但我无法确定根本原因。 自定义对象是通过相同的依赖项间接拉取的,并且没有实现 equals() 和 hashcode(),它只有
private static final long serialVersionUID = 1L;
查看自定义对象的源代码揭示了此实现
public class CustomObject extends BaseModel implements Serializable
和 BaseModel 定义了 equals 和 hashCode 方法
import org.jvnet.jaxb2_commons.lang.*;
import org.jvnet.jaxb2_commons.locator.ObjectLocator;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlType;
import java.io.Serializable;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "BaseModel")
@XmlSeeAlso({
CustomObject.class
})
public abstract class BaseModel implements Serializable, Equals2, HashCode2
{
private final static long serialVersionUID = 1L;
public boolean equals(ObjectLocator thisLocator, ObjectLocator thatLocator, Object object, EqualsStrategy2 strategy) {
if ((object == null)||(this.getClass()!= object.getClass())) {
return false;
}
if (this == object) {
return true;
}
return true;
}
public boolean equals(Object object) {
final EqualsStrategy2 strategy = JAXBEqualsStrategy.INSTANCE;
return equals(null, null, object, strategy);
}
public int hashCode(ObjectLocator locator, HashCodeStrategy2 strategy) {
int currentHashCode = 1;
return currentHashCode;
}
public int hashCode() {
final HashCodeStrategy2 strategy = JAXBHashCodeStrategy.INSTANCE;
return this.hashCode(null, strategy);
}
}
提前谢谢你。
显然,基类中发生了一些变化,您只需要找到它并修复它,或者在这个类中实现hashCode()
和equals()
可以接受。
某个地方有人实现了返回 1 的hashCode()
,这是愚蠢的。他们最好不要实施它。而且不难找到。只要看看Javadoc的CustomObject
,看看它从哪里继承hashCode()
。
我想你已经意识到你的问题的答案是什么,但是你在更新中添加的代码清楚地表明了这一点:
-
您的
CustomClass
扩展了BaseClass
。 -
BaseClass
将覆盖Object::hashCode()
-
您向我们展示的
BaseClass
版本中的覆盖将始终返回 1。 它调用具有特定策略的hashCode(ObjectLocator, HashCodeStrategy2)
方法,但该方法的实现只是忽略了策略参数。
现在很清楚,该版本的BaseClass
代码只能返回 1 作为哈希码。 但是你说你的代码曾经工作过,你只是改变了依赖关系。 由此,我们必须得出结论,依赖项已更改,并且新版本的依赖项已损坏。
如果这有什么"奇怪"的地方,那就是有人决定像这样实现(新)BaseClass
,并在没有正确测试的情况下发布它。
实际上,有一种可能的方法可以让您的CustomClass
工作。BaseClass::hashCode(ObjectLocator, HashCodeStrategy2)
方法是公共的,不是最终的,因此您可以在CustomClass
上覆盖它,如下所示:
@Override
public int hashCode(ObjectLocator locator, HashCodeStrategy2 strategy) {
return System.identityHashCode(this);
}
事实上,可能是BaseClass
的实施者打算让你这样做。 但我仍然认为BaseClass
是破碎的:
- 对类进行编码,以便
hashCode
返回 1 作为默认行为是令人讨厌的。 BaseClass
中没有javadoc来解释覆盖该方法的必要性。- 他们对
BaseClass
行为的(未事先通知?)更改是API 中断性更改。 你不应该做这样的事情,没有很好的理由,没有警告。 - 相应的
equals
方法的默认行为在客观上是错误的。
两个不相等的对象具有相同的哈希代码是可以的。事实上,这是一个数学要求,这是允许的。例如,考虑字符串:有无限多个不相等的字符串("a","aa","aaa"...),但只有2^32个可能的整数值。显然,必须有不同的字符串共享哈希代码。
但是HashSet知道这一点,所以它使用equals
的结果以及哈希代码。如果只添加其中一个对象,那么它们不仅具有相同的哈希代码 - 它们是相等的,由equals
方法返回。如果没有自定义类的代码,我们无法确定为什么会这样,更不用说它是否是故意的了。
Object 的合约规定,相等的对象必须具有相同的哈希代码。但反之则不然:具有相同哈希代码的对象不必相等。Javadocs明确表示:
- 如果根据equals(java.lang.Object) 方法,两个对象不相等,则对两个对象中的每一个调用 hashCode 方法必须产生不同的整数结果,则不需要。但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。
除非类的文档明确告诉您它如何计算其哈希代码,否则您可能不应将其视为已建立的联系人,并且您应该期望它可能会在版本之间更改。
CustomObject
类实现(或其祖先之一)是这里的问题。CustomObject
的作者(或其祖先之一)以错误的方式覆盖了toString
、hashCode
和equals
方法,而不理解它的语义和含义。以下是解决问题的选项(不一定按该顺序):
- 您应该将类中的问题通知依赖库
CustomObject
作者,并以正确的方式实现或覆盖toString
、hashCode
和equals
方法。但请记住 - 依赖代码作者将来可以再次将您放回这个地方。 - 假设
CustomObject
类不是final
- 扩展CustomObject
(请注意 - 它最好命名为CustomClass
,而不是CustomObject
),以便正确实现toString
,hashCode
和equals
方法。在代码中使用此扩展类,而不是CustomObject
类。这将给你更好的控制,因为你的依赖代码不会再让你陷入这种麻烦。 - 使用
AOP
在类中引入toString
、hashCode
和equals
方法的重写和正确实现CustomObject
。这种方法也像上面的选项 2 一样面向未来。