在 Java 中,hashCode()
方法的主要作用就是为了配合哈希表使用的。
哈希表(Hash Table),也叫散列表,是一种可以通过关键码值(key-value)直接访问的数据结构,它最大的特点就是可以快速实现查找、插入和删除。其中用到的算法叫做哈希,就是把任意长度的输入,变换成固定长度的输出,该输出就是哈希值。像 MD5、SHA1 都用的是哈希算法。
像 Java 中的 HashSet、Hashtable(注意是小写的 t)、HashMap 都是基于哈希表的具体实现。其中的 HashMap 就是最典型的代表,不仅面试官经常问,工作中的使用频率也非常的高。
大家想一下,如果没有哈希表,但又需要这样一个数据结构,它里面存放的数据是不允许重复的,该怎么办呢?
要不使用 equals()
方法进行逐个比较?这种方案当然是可行的。但如果数据量特别特别大,采用 equals()
方法进行逐个对比的效率肯定很低很低,最好的解决方案就是哈希表。
拿 HashMap 来说吧。当我们要在它里面添加对象时,先调用这个对象的 hashCode()
方法,得到对应的哈希值,然后将哈希值和对象一起放到 HashMap 中。当我们要再添加一个新的对象时:
-
获取对象的哈希值;
-
和之前已经存在的哈希值进行比较,如果不相等,直接存进去;
-
如果有相等的,再调用
equals()
方法进行对象之间的比较,如果相等,不存了; -
如果不等,说明哈希冲突了,增加一个链表,存放新的对象;
-
如果链表的长度大于 8,转为红黑树来处理。
就这么一套下来,调用 equals()
方法的频率就大大降低了。也就是说,只要哈希算法足够的高效,把发生哈希冲突的频率降到最低,哈希表的效率就特别的高。
来看一下 HashMap 的哈希算法:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
先调用对象的 hashCode()
方法,然后对该值进行右移运算,然后再进行异或运算。
通常来说,String 会用来作为 HashMap 的键进行哈希运算,因此我们再来看一下 String 的 hashCode()
方法:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
-
hash = h = isLatin1() ? StringLatin1.hashCode(value)
- StringUTF16.hashCode(value);
}
return h;
}
public static int hashCode(byte[] value) {
int h = 0;
int length = value.length >> 1;
for (int i = 0; i < length; i++) {
h = 31 * h + getChar(value, i);
}
return h;
}
可想而知,经过这么一系列复杂的运算,再加上 JDK 作者这种大师级别的设计,哈希冲突的概率我相信已经降到了最低。
当然了,从理论上来说,对于两个不同对象,它们通过 hashCode()
方法计算后的值可能相同。因此,不能使用 hashCode()
方法来判断两个对象是否相等,必须得通过 equals()
方法。
也就是说:
-
如果两个对象调用
equals()
方法得到的结果为 true,调用hashCode()
方法得到的结果必定相等; -
如果两个对象调用
hashCode()
方法得到的结果不相等,调用equals()
方法得到的结果必定为 false;
反之:
-
如果两个对象调用
equals()
方法得到的结果为 false,调用hashCode()
方法得到的结果不一定不相等; -
如果两个对象调用
hashCode()
方法得到的结果相等,调用equals()
方法得到的结果不一定为 true;
来看下面这段代码。
public class Test {
public static void main(String[] args) {
Student s1 = new Student(18, “张三”);
Map<Student, Integer> scores = new HashMap<>();
scores.put(s1, 98);
System.out.println(scores.get(new Student(18, “张三”)));
}
}
class Student {
private int age;
private String name;
public Student(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public boolean equals(Object o) {
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
}
我们重写了 Student 类的 equals()
方法,如果两个学生的年纪和姓名相同,我们就认为是同一个学生,虽然很离谱,但我们就是这么草率。
在 main()
方法中,18 岁的张三考试得了 98 分,很不错的成绩,我们把张三和成绩放到了 HashMap 中,然后准备输出张三的成绩:
null
很不巧,结果为 null,而不是预期当中的 98。这是为什么呢?
原因就在于重写 equals()
方法的时候没有重写 hashCode()
方法。默认情况下,hashCode()
方法是一个本地方法,会返回对象的存储地址,显然 put()
中的 s1 和 get()
中的 new Student(18, "张三")
是两个对象,它们的存储地址肯定是不同的。
HashMap 的 get()
方法会调用 hash(key.hashCode())
计算对象的哈希值,虽然两个不同的 hashCode()
结果经过 hash()
方法计算后有可能得到相同的结果,但这种概率微乎其微,所以就导致 scores.get(new Student(18, "张三"))
无法得到预期的值 18。
怎么解决这个问题呢?很简单,重写 hashCode()
方法。
@Override
public int hashCode() {
return Objects.hash(age, name);
}
Objects 类的 hash()
方法可以针对不同数量的参数生成新的 hashCode()
值。
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;
}
代码似乎很简单,归纳出的数学公式如下所示(n 为字符串长度)。
注意:31 是个奇质数,不大不小,一般质数都非常适合哈希计算,偶数相当于移位运算,容易溢出,造成数据信息丢失。
这就意味着年纪和姓名相同的情况下,会得到相同的哈希值。scores.get(new Student(18, "张三"))
就会返回 98 的预期值了。
最后
学习视频:
大厂面试真题:
b836834809cf921db848aa4c129d4.png)
注意:31 是个奇质数,不大不小,一般质数都非常适合哈希计算,偶数相当于移位运算,容易溢出,造成数据信息丢失。
这就意味着年纪和姓名相同的情况下,会得到相同的哈希值。scores.get(new Student(18, "张三"))
就会返回 98 的预期值了。
最后
学习视频:
[外链图片转存中…(img-HDrtu9Dk-1714737952161)]
大厂面试真题:
[外链图片转存中…(img-DgbYvuAC-1714737952162)]