文章目录
一、什么是equals()
我们先来看下再熟悉不过的equals方法。以下是java api对equals()方法的说明:
从上面关于equals的说明,我们可以得出以下几点:
-
equals用来判断两个对象是否相等。在判断时分为两种情况:
1.1 对象所属类没有重写该方法,这时比较的是对象的地址,即引用是否相等。
1.2 对象所属类重写了该方法,这时比较的是对象的内容,即对象内容相同时,equals就返回true。 -
当重写了equals方法时,必须重写hashCode方法,以便遵守hashCode方法的规则(相等的对象必须具有相等的哈希码)。
二、什么是hashCode()
hashCode()是Object类中定义的一个方法。以下是java api对hashCode方法的说明:
我们可以看出以下几点:
- hashCode方法返回的是对象的哈希值。支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表。其实是为了提高查找效率,hashCode的主要作用就是用来在散列存储结构中确定对象的存储地址的。
- 同一对象的哈希值唯一。
- 若使用equals(Object)方法比较的两个对象相等,则这两个对象的哈希值相等。
- 若使用equals(Object)方法比较的两个对象不相等,则这两个对象的哈希值也可能相等。
- equals返回true时,哈希值一定相等;而哈希值相等时,equals返回值不确定,即对象不一定相等。
也就是说任何类中实现的hashCode方法都需要遵守这些规则,其实就是上面的第5点。那么,为什么equals返回true时,哈希值一定相等呢?而哈希值相等时,equals返回值反而不确定呢?hashCode又是如何得到的呢?实践出真知,下面我们从源码和案例的角度来看下。
三、hashCode是如何得到的
1. 类重写了hashCode()
- hashCode返回的是一个整数,可以通过它得到在散列存储结构中,这个对象的存储地址。比如HashSet中就是根据hashcode再计算得到元素在底层数组中的位置的。
- 以下我们先创建一个重写了equals和hashCode方法的Person类:
import java.util.Objects;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
- 以下是测试类
public class HashCodeTest {
public static void main(String[] args) {
Person person1 = new Person("Tom", 12);
Person person2 = new Person("Tom", 12);
Person person3;
person3 = person1;
String str = "a";
Integer num1 = 97;
System.out.println(str.hashCode() + " " + num1.hashCode());//说明hashcode相等,equals不一定相等
System.out.println(person1.hashCode() + " " + person2.hashCode());
System.out.println(person3.hashCode() == person1.hashCode());//true,因为内存地址一样
}
}
- 以下是输出结果
97 97
2613467 2613467
true
- 首先对于str对象和num1对象,为什么他们的哈希值相等呢?我们可以分别看一下String类和Integer类中对hashCode的实现。首先是String类中关于hashCode()的源码:
/**
* Returns a hash code for this string. The hash code for a
* {@code String} object is computed as
* <blockquote><pre>
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* </pre></blockquote>
* using {@code int} arithmetic, where {@code s[i]} is the
* <i>i</i>th character of the string, {@code n} is the length of
* the string, and {@code ^} indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/
public int hashCode() {
int h = hash;
final int len = length();
if (h == 0 && len > 0) {
for (int i = 0; i < len; i++) {
h = 31 * h + charAt(i);
}
hash = h;
}
return h;
}
- 可以看出,String中生成对象的哈希码主要是依据字符串对象中的每个字符计算出来的。
- 从下面Integer类中hashCode的源码可以看出,它是直接将Integer对象的值作为哈希值的。所以上面例子中str和num1的哈希值都是97。而如果用equals对它们进行比较,肯定返回false。这就证明了哈希值相等时,对象不一定相等。
@Override
public int hashCode() {
return Integer.hashCode(value);
}
/**
* Returns a hash code for a {@code int} value; compatible with
* {@code Integer.hashCode()}.
*
* @param value the value to hash
* @since 1.8
*
* @return a hash code value for a {@code int} value.
*/
public static int hashCode(int value) {
return value;
}
- 那么对于对象person1和person2,为什么他们的哈希值也相等呢?我们先看下Person类重写的hashCode的方法的底层源码:
public static int hash(Object... values) {
return Arrays.hashCode(values);
}
public static int hashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}
它与上面String类中hashCode的实现原理类似,都是根据对象里的每个属性计算得到,也是将素数31作为乘法器。因此,由于person1和person2的内容相同,它们的哈希值也就是一样的了。而且这时也是遵守hashCode的规则的,因为Person类重写了equals方法,所以此时用equals比较这两个对象,仍然是相等的。这就证明了equals相等,hashCode一定相等。当然前面所有观点成立的前提都是类中重写了hashCode()和equals()。
2. 类没有重写hashCode()
- 那么如果类中没有重写hashCode方法呢,这时hashcode又是如何生成的呢?
import java.util.Objects;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
- 我们将例子中重写的hashCode方法和equals方法去除,得到的Person类如上所示。这时得到的哈希值和上面的不一样了:
97 97
1163157884 1956725890
true
- 实际上, 当 自 定 义 类 没 重 写 h a s h C o d e ( ) 时 , 哈 希 值 是 根 据 对 象 在 内 存 中 的 地 址 生 成 的 \color{red}{当自定义类没重写hashCode()时,哈希值是根据对象在内存中的地址生成的} 当自定义类没重写hashCode()时,哈希值是根据对象在内存中的地址生成的。从下面的源码看出,对象调用的是Object类的原生的hash方法,然后在hash方法中再调用系统的identityHashCode方法来生成哈希值,identityHashCode方法永远返回根据对象物理内存地址产生的hash值,所以由于person1和person2对象的物理地址不一样,最后生成的哈希值也会不一样。
// public native int hashCode();
public int hashCode() {
return identityHashCode(this);
}
// Package-private to be used by j.l.System. We do the implementation here
// to avoid Object.hashCode doing a clinit check on j.l.System, and also
// to avoid leaking shadow$_monitor_ outside of this class.
/* package-private */ static int identityHashCode(Object obj) {
int lockWord = obj.shadow$_monitor_;
final int lockWordStateMask = 0xC0000000; // Top 2 bits.
final int lockWordStateHash = 0x80000000; // Top 2 bits are value 2 (kStateHash).
final int lockWordHashMask = 0x0FFFFFFF; // Low 28 bits.
if ((lockWord & lockWordStateMask) == lockWordStateHash) {
return lockWord & lockWordHashMask;
}
return identityHashCodeNative(obj);
}
我们可以做一下验证。如果在测试类中添加以下内容,会发现它和person1.hashCode()的结果是相同的。
System.out.println(System.identityHashCode(person1));
四、总结
1. hashCode()和equals()的比较
- 当类重写它们时,hashCode()返回的是根据对象的内容(属性)计算出的一个整数;equals()判断的是对象的内容是否相等。
- 比较两个对象是否相等时,hashCode()相等,equals()不一定相等,因此不可靠; e q u a l s ( ) 相 等 时 , h a s h C o d e ( ) 一 定 相 等 \color{red}{ equals()相等时,hashCode()一定相等} equals()相等时,hashCode()一定相等。
2. 为何hashCode()和equals()搭配使用比较对象时能提高效率
- 当成员变量很多时, 只 用 e q u a l s ( ) 比 较 , 方 法 内 部 会 涉 及 大 量 的 操 作 \color{red}{ 只用equals()比较,方法内部会涉及大量的操作} 只用equals()比较,方法内部会涉及大量的操作,效率低,所以可以先判断hashCode()值,若不相等,则对象肯定不相等;否则继续用equals()判断。