equals() 和 hashCode() 都是属于Object类的方法。
先说明它们之间的关系:
若两个对象equals(Object obj)返回true,则hashCode()有必要也返回相同的int数。
若两个对象equals(Object obj)返回false,则hashCode()有可能是相同的int数,也有可能不相同。
若两个对象hashCode()返回相同int数,则equals(Object obj)不一定返回true。
若两个对象hashCode()返回不同int数,则equals(Object obj)一定返回false。
看一下它们的源码解释:
equals
* Note that it is generally necessary to override the {@code hashCode}
* method whenever this method is overridden, so as to maintain the
* general contract for the {@code hashCode} method, which states
* that equal objects must have equal hash codes.
*
* @param obj the reference object with which to compare.
* @return {@code true} if this object is the same as the obj
* argument; {@code false} otherwise.
* @see #hashCode()
* @see java.util.HashMap
*/
public boolean equals(Object obj) {
return (this == obj);
}
这注释说了,如果equals被重写了,务必hashCode方法也需要被重写,因为equals相等的两个对象,它们的hashCode必须是相等的。这里注意equals()里头用的是==,比较的是内存地址。
hashCode
/**
* Returns a hash code value for the object. This method is
* supported for the benefit of hash tables such as those provided by
* {@link java.util.HashMap}.
*
* The general contract of {@code hashCode} is:
*
* - Whenever it is invoked on the same object more than once during
* an execution of a Java application, the {@code hashCode} method
* must consistently return the same integer, provided no information
* used in {@code equals} comparisons on the object is modified.
* This integer need not remain consistent from one execution of an
* application to another execution of the same application.
*
- If two objects are equal according to the {@code equals(Object)}
* method, then calling the {@code hashCode} method on each of
* the two objects must produce the same integer result.
*
- It is not required that if two objects are unequal
* according to the {@link java.lang.Object#equals(java.lang.Object)}
* method, then calling the {@code hashCode} method on each of the
* two objects must produce distinct integer results. However, the
* programmer should be aware that producing distinct integer results
* for unequal objects may improve the performance of hash tables.
*
*
* As much as is reasonably practical, the hashCode method defined by
* class {@code Object} does return distinct integers for distinct
* objects. (This is typically implemented by converting the internal
* address of the object into an integer, but this implementation
* technique is not required by the
* Java™ programming language.)
*
* @return a hash code value for this object.
* @see java.lang.Object#equals(java.lang.Object)
* @see java.lang.System#identityHashCode
*/
public native int hashCode();
里面说hashCode()返回是一个int类型的值,是由对象的内存地址生成而来。里面还定义了3条守则:
- 一个对象无论调用它的hashCode方法多少次,都会返回相同的integer(哈希值)。
- 两个对象如果通过equals方法判定为相等,那么就应当返回相同integer(哈希值)。
- 两个地址值不相等的对象调用hashCode方法不要求返回不相等的integer(哈希值),但是要求拥有两个不相等integer的对象必须是不同对象。
hashCode主要作用于hashMap、hashSet、hashTable这些散列数据结构中。
现在就拿hashMap的源码来举例:
可以看到,相同key替换的时候,用到逻辑是hash相同而且要equals,不然就替换失败,而hash也要用到key的hashCode()来计算。
如果某个对象需要用到hash类型的数据结构工具时,而又没有重写,就会产生非常严重的问题,当然如果用不到,可以不写,但最好还是写上,因为在程序的编写过程,不保证不会用这些hash结构数据工具。
佐证
/**
* 重写equals必须要重写hashCode
*
* 使用hashSet来判断,hashSet底层还是hashMap,putVal会判断hash和equals是否都相同,才认为是同一个元素key
*
* @author Administrator
*/
public class OverrideHashEqualsTs {
public static void main(String[] args) {
System.out.println("==========================既不重写equals也不重写hashCode==========================");
HashSet overrideNeitherEqualsOrHash_hashSet = new HashSet();
OverrideNeitherEqualsOrHash overrideNeitherEqualsOrHash_1 = new OverrideNeitherEqualsOrHash(1, "1");
OverrideNeitherEqualsOrHash overrideNeitherEqualsOrHash_2 = new OverrideNeitherEqualsOrHash(1, "1");
overrideNeitherEqualsOrHash_hashSet.add(overrideNeitherEqualsOrHash_1);
overrideNeitherEqualsOrHash_hashSet.add(overrideNeitherEqualsOrHash_2);
overrideNeitherEqualsOrHash_hashSet.add(overrideNeitherEqualsOrHash_1);
System.out.println("overrideNeitherEqualsOrHash_1 hashCode : " + overrideNeitherEqualsOrHash_1.hashCode()
+ " , overrideNeitherEqualsOrHash_2 hashCode : " + overrideNeitherEqualsOrHash_2.hashCode()
+ ", they equals ? " + overrideNeitherEqualsOrHash_1.equals(overrideNeitherEqualsOrHash_2)
+ ", hashSet : " + overrideNeitherEqualsOrHash_hashSet
);
/* 结果:哈都没重写,该覆盖的覆盖
* overrideNeitherEqualsOrHash_1 hashCode : 685325104 , overrideNeitherEqualsOrHash_2 hashCode : 460141958,
* they equals ? false,
* hashSet : [OverrideNeitherEqualsOrHash{age=1, name='1', hashCode='685325104},
* OverrideNeitherEqualsOrHash{age=1, name='1', hashCode='460141958}]
*/
System.out.println();
System.out.println("==========================只重写equals不重写hashCode==========================");
HashSet overrideEqualsNotHash_hashSet = new HashSet();
OverrideEqualsNotHash overrideEqualsNotHash_1 = new OverrideEqualsNotHash(1, "1");
OverrideEqualsNotHash overrideEqualsNotHash_2 = new OverrideEqualsNotHash(1, "1");
overrideEqualsNotHash_hashSet.add(overrideEqualsNotHash_1);
overrideEqualsNotHash_hashSet.add(overrideEqualsNotHash_2);
overrideEqualsNotHash_hashSet.add(overrideEqualsNotHash_1);
System.out.println("overrideEqualsNotHash_1 hashCode : " + overrideEqualsNotHash_1.hashCode()
+ " , overrideEqualsNotHash_2 hashCode : " + overrideEqualsNotHash_2.hashCode()
+ ", they equals ? " + overrideEqualsNotHash_1.equals(overrideEqualsNotHash_2)
+ ", hashSet : " + overrideEqualsNotHash_hashSet
);
/* 结果:只是重写equals没有重写hashCode,hashSet底层还是hashMap,putVal会判断hash和equals是否都相同,才认为是同一个元素key
* overrideEqualsNotHash_1 hashCode : 1163157884 , overrideEqualsNotHash_2 hashCode : 1956725890,
* they equals ? true,
* hashSet : [OverrideEqualsNotHash{age=1, name='1', hashCode='1956725890},
* OverrideEqualsNotHash{age=1, name='1', hashCode='1163157884}]
*/
System.out.println();
System.out.println("==========================只重写hashCode不重写equals==========================");
HashSet overrideHashNotEquals_hashSet = new HashSet();
OverrideHashNotEquals overrideHashNotEquals_1 = new OverrideHashNotEquals(1, "1");
OverrideHashNotEquals overrideHashNotEquals_2 = new OverrideHashNotEquals(1, "1");
overrideHashNotEquals_hashSet.add(overrideHashNotEquals_1);
overrideHashNotEquals_hashSet.add(overrideHashNotEquals_2);
overrideHashNotEquals_hashSet.add(overrideHashNotEquals_1);
System.out.println("overrideHashNotEquals_1 hashCode : " + overrideHashNotEquals_1.hashCode()
+ " , overrideHashNotEquals_2 hashCode : " + overrideHashNotEquals_2.hashCode()
+ ", they equals ? " + overrideHashNotEquals_1.equals(overrideHashNotEquals_2)
+ ", hashSet : " + overrideHashNotEquals_hashSet
);
/* 结果:只是重写hashCode没有重写equals,hashSet底层还是hashMap,putVal会判断hash和equals是否都相同,才认为是同一个元素key
* overrideHashNotEquals_1 hashCode : 80 , overrideHashNotEquals_2 hashCode : 80,
* they equals ? false.
* hashSet : [OverrideHashNotEquals{age=1, name='1', hashCode='80},
* OverrideHashNotEquals{age=1, name='1', hashCode='80}]
*/
System.out.println();
System.out.println("==========================既重写equals也重写hashCode==========================");
HashSet OverrideEqualsAndHash_hashSet = new HashSet();
OverrideEqualsAndHash overrideEqualsAndHash_1 = new OverrideEqualsAndHash(1, "1");
OverrideEqualsAndHash overrideEqualsAndHash_2 = new OverrideEqualsAndHash(1, "1");
OverrideEqualsAndHash_hashSet.add(overrideEqualsAndHash_1);
OverrideEqualsAndHash_hashSet.add(overrideEqualsAndHash_2);
OverrideEqualsAndHash_hashSet.add(overrideEqualsAndHash_1);
System.out.println("overrideEqualsAndHash_1 hashCode : " + overrideEqualsAndHash_1.hashCode()
+ " , overrideEqualsAndHash_2 hashCode : " + overrideEqualsAndHash_2.hashCode()
+ ", they equals ? " + overrideEqualsAndHash_1.equals(overrideEqualsAndHash_2)
+ ", hashSet : " + OverrideEqualsAndHash_hashSet
);
/* 结果:既重写equals也重写hashCode,add进去的都当做是相同的元素,所以都是覆盖,hashSet最后最后一个元素
* overrideEqualsAndHash_1 hashCode : 80 , overrideEqualsAndHash_2 hashCode : 80,
* they equals ? true,
* hashSet : [OverrideEqualsAndHash{age=1, name='1', hashCode='80}]
*/
}
static class OverrideNeitherEqualsOrHash {
Integer age;
String name;
public OverrideNeitherEqualsOrHash(Integer age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "OverrideNeitherEqualsOrHash{" +
"age=" + age +
", name='" + name + '\'' +
", hashCode='" + hashCode() +
'}';
}
}
static class OverrideEqualsNotHash {
Integer age;
String name;
public OverrideEqualsNotHash(Integer age, String name) {
this.age = age;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
OverrideEqualsNotHash that = (OverrideEqualsNotHash) o;
if (age != null ? !age.equals(that.age) : that.age != null) {
return false;
}
return name != null ? name.equals(that.name) : that.name == null;
}
@Override
public String toString() {
return "OverrideEqualsNotHash{" +
"age=" + age +
", name='" + name + '\'' +
", hashCode='" + hashCode() +
'}';
}
}
static class OverrideHashNotEquals {
Integer age;
String name;
public OverrideHashNotEquals(Integer age, String name) {
this.age = age;
this.name = name;
}
@Override
public int hashCode() {
int result = age != null ? age.hashCode() : 0;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "OverrideHashNotEquals{" +
"age=" + age +
", name='" + name + '\'' +
", hashCode='" + hashCode() +
'}';
}
}
static class OverrideEqualsAndHash {
Integer age;
String name;
public OverrideEqualsAndHash(Integer age, String name) {
this.age = age;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OverrideEqualsAndHash that = (OverrideEqualsAndHash) o;
if (age != null ? !age.equals(that.age) : that.age != null) return false;
return name != null ? name.equals(that.name) : that.name == null;
}
@Override
public int hashCode() {
int result = age != null ? age.hashCode() : 0;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "OverrideEqualsAndHash{" +
"age=" + age +
", name='" + name + '\'' +
", hashCode='" + hashCode() +
'}';
}
}
}
结论:
在使用hashSet、hashMap这种hash结构的数据工具时,放置的key如果重写了equals就必须重写hashCode,只有这样才会视为同一个key,不违反对象相等必须有相同的hashCode的原则。
关于hashMap内存泄露的问题
皆因都是因为hashCode和equals重写的原因,改变了对象的属性就改变了其hashCode,导致找不到桶位,get出来自然就是null,然后原来位置的数据并没有被清除,造成内存泄露。
代码例子
/**
* 测试下改变对象是否会改变其hashCode
*
* @author Administrator
*/
public class HashCodeWillChangeTs {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
Object value = new Object();
Integer integer = new Integer(5);
//integer重写hashCode方法,hashCode就等于value
System.out.println(integer.hashCode()); // 5
hashMap.put(integer, value);
System.out.println(hashMap);//5
integer = 128;
System.out.println(integer.hashCode()); // 128,integer是不可变类,这样等于指向另外一个对象
System.out.println(hashMap.get(integer));// null,已经换了对象了,肯定找不出来了,内存泄露了
hashMap.clear();
System.out.println("=======================");
//数组是可变对象
String[] strAy = new String[]{"1", "2", "3"};
System.out.println(strAy.hashCode()); //460141958
hashMap.put(strAy, value);
hashMap.forEach((k, v) ->System.out.println(Arrays.toString((String[])k) + "--" + v));//[1, 2, 3]--java.lang.Object@28d93b30
strAy[2] = "44";
System.out.println(strAy.hashCode()); //460141958,strAry指向堆中一块内存,内存地址一直没变,变的是那块内存上的数据
System.out.println(hashMap.get(strAy));//java.lang.Object@28d93b30,依然能拿出来,它的hashCode、equals本质没有重写过。
hashMap.clear();
System.out.println("=======================");
//没有重写hashCode和equals的自定义类
DemoClazz demoClazz = new DemoClazz(4, "demoClazz");
System.out.println(demoClazz.hashCode());//460141958
hashMap.put(demoClazz,value);
System.out.println(hashMap);//{DemoClazz{age=4, name='demoClazz'}=java.lang.Object@28d93b30}
demoClazz.setName("fakeDemoClazz");
System.out.println(demoClazz.hashCode());//460141958,和上面同样的道理
System.out.println(hashMap.get(demoClazz));//java.lang.Object@28d93b30
hashMap.clear();
System.out.println("=======================");
//只重写hashCode没有重写equals的自定义类
DemoClazzOverHashCode demoClazzOverHashCode = new DemoClazzOverHashCode(99, "demoClazzOverHashCode");
System.out.println(demoClazzOverHashCode.hashCode());//822272993
hashMap.put(demoClazzOverHashCode,value);
System.out.println(hashMap);//{DemoClazzOverHashCode{age=99, name='demoClazzOverHashCode'}=java.lang.Object@28d93b30}
demoClazzOverHashCode.setName("fakeDemoClazzOverHashCode");
System.out.println(demoClazzOverHashCode.hashCode());//-617106548,hashCode已经变化
System.out.println(hashMap.get(demoClazzOverHashCode));//null,找不到桶的位置了,造成内存泄露
hashMap.clear();
System.out.println("=======================");
//既重写hashCode也重写equals的自定义类
DemoClazzOverHashAndEquals demoClazzOverHashAndEquals = new DemoClazzOverHashAndEquals(88, "demoClazzOverHashAndEquals");
System.out.println(demoClazzOverHashAndEquals.hashCode());//-962172025
hashMap.put(demoClazzOverHashAndEquals,value);
System.out.println(hashMap);//{DemoClazzOverHashAndEquals{age=88, name='demoClazzOverHashAndEquals'}=java.lang.Object@28d93b30}
demoClazzOverHashAndEquals.setName("fakeDemoClazzOverHashAndEquals");
System.out.println(demoClazzOverHashAndEquals.hashCode());//-1142444356,hashCode已经变化
System.out.println(hashMap.get(demoClazzOverHashAndEquals));//null,找不到桶的位置了,造成内存泄露
}
static class DemoClazz {
private Integer age;
private String name;
public DemoClazz(Integer age, String name) {
this.age = age;
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "DemoClazz{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
static class DemoClazzOverHashCode {
private Integer age;
private String name;
public DemoClazzOverHashCode(Integer age, String name) {
this.age = age;
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
@Override
public int hashCode() {
int result = age.hashCode();
result = 31 * result + name.hashCode();
return result;
}
@Override
public String toString() {
return "DemoClazzOverHashCode{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
static class DemoClazzOverHashAndEquals{
private Integer age;
private String name;
public DemoClazzOverHashAndEquals(Integer age, String name) {
this.age = age;
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DemoClazzOverHashAndEquals that = (DemoClazzOverHashAndEquals) o;
if (!age.equals(that.age)) return false;
return name.equals(that.name);
}
@Override
public int hashCode() {
int result = age.hashCode();
result = 31 * result + name.hashCode();
return result;
}
@Override
public String toString() {
return "DemoClazzOverHashAndEquals{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
}
关于尽量使用不可变对象作为hashMap等hash结构数据工具的key
因为不可变对象,如integer、string等,一般都重写了hashCode和equals方法。变换了值,那hashCode肯定就是不同了,也get不出东西来。
如果一定要使用可变对象作为key,就需要保证该对象的属性发生改变时,不会改变对象的hashcode值。
参考:
hashCode和equals方法的区别与联系
重写equals就得hashCode方法原因的探究