今天咱们来谈谈 Java 中的 hashCode()
方法——经过源码的角度。众所周知,Java 是一门面向对象的编程语言,全部的类都会默认继承自 Object 类,而 Object 的中文意思就是“对象”。java
Object 类中就包含了 hashCode()
方法:git
@HotSpotIntrinsicCandidate
public native int hashCode();
复制代码
意味着全部的类都会有一个 hashCode()
方法,该方法会返回一个 int 类型的值。因为 hashCode()
方法是一个本地方法(native
关键字修饰的方法,用 C/C++
语言实现,由 Java 调用),意味着 Object 类中并无给出具体的实现。github
具体的实现能够参考 jdk/src/hotspot/share/runtime/synchronizer.cpp
(源码能够到 GitHub 上 OpenJDK 的仓库中下载)。get_next_hash()
方法会根据 hashCode 的取值来决定采用哪种哈希值的生成策略。面试
而且 hashCode()
方法被 @HotSpotIntrinsicCandidate
注解修饰,说明它在 HotSpot 虚拟机中有一套高效的实现,基于 CPU 指令。算法
我在 掘金 共输出了 100 多篇 Java 方面的文章,总字数超过 30 万字, 内容风趣幽默、通俗易懂,收获了不少初学者的承认和支持,内容包括 Java 语法、Java 集合框架、Java 并发编程、Java 虚拟机等核心内容。 为了帮助更多的 Java 初学者,我“一怒之下”就把这些文章从新整理并开源到了 GitHub,起名《教妹学 Java》,听起来是否是就颇有趣?编程
GitHub 开源地址(欢迎 star):github.com/itwanger/jm…安全
你们有没有想过这样一个问题:为何 Object 类须要一个 hashCode()
方法呢?markdown
在 Java 中,hashCode()
方法的主要做用就是为了配合哈希表使用的。数据结构
哈希表(Hash Table),也叫散列表,是一种能够经过关键码值(key-value)直接访问的数据结构,它最大的特色就是能够快速实现查找、插入和删除。其中用到的算法叫作哈希,就是把任意长度的输入,变换成固定长度的输出,该输出就是哈希值。像 MD五、SHA1 都用的是哈希算法。多线程
像 Java 中的 HashSet、Hashtable(注意是小写的 t)、HashMap 都是基于哈希表的具体实现。其中的 HashMap 就是最典型的表明,不只面试官常常问,工做中的使用频率也很是的高。
你们想一下,若是没有哈希表,但又须要这样一个数据结构,它里面存放的数据是不容许重复的,该怎么办呢?
要不使用 equals()
方法进行逐个比较?这种方案固然是可行的。但若是数据量特别特别大,采用 equals()
方法进行逐个对比的效率确定很低很低,最好的解决方案就是哈希表。
拿 HashMap 来讲吧。当咱们要在它里面添加对象时,先调用这个对象的 hashCode()
方法,获得对应的哈希值,而后将哈希值和对象一块儿放到 HashMap 中。当咱们要再添加一个新的对象时:
equals()
方法进行对象之间的比较,若是相等,不存了;就这么一套下来,调用 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 的预期值了。
《Java 编程思想》这本圣经中有一段话,对 hashCode()
方法进行了一段描述。
设计
hashCode()
时最重要的因素就是:不管什么时候,对同一个对象调用hashCode()
都应该生成一样的值。若是在将一个对象用put()
方法添加进 HashMap 时产生一个hashCode()
值,而用get()
方法取出时却产生了另一个hashCode()
值,那么就没法从新取得该对象了。因此,若是你的hashCode()
方法依赖于对象中易变的数据,用户就要小心了,由于此数据发生变化时,hashCode()
就会生成一个不一样的哈希值,至关于产生了一个不一样的键。
也就是说,若是在重写 hashCode()
和 equals()
方法时,对象中某个字段容易发生改变,那么最好舍弃这些字段,以避免产生不可预期的结果。
好。有了上面这些内容做为基础后,咱们回头再来看看本地方法 hashCode()
的 C++ 源码。
static inline intptr_t get_next_hash(Thread* current, oop obj) {
intptr_t value = 0;
if (hashCode == 0) {
// This form uses global Park-Miller RNG.
// On MP system we'll have lots of RW access to a global, so the
// mechanism induces lots of coherency traffic.
value = os::random();
} else if (hashCode == 1) {
// This variation has the property of being stable (idempotent)
// between STW operations. This can be useful in some of the 1-0
// synchronization schemes.
intptr_t addr_bits = cast_from_oop<intptr_t>(obj) >> 3;
value = addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random;
} else if (hashCode == 2) {
value = 1; // for sensitivity testing
} else if (hashCode == 3) {
value = ++GVars.hc_sequence;
} else if (hashCode == 4) {
value = cast_from_oop<intptr_t>(obj);
} else {
// Marsaglia's xor-shift scheme with thread-specific state
// This is probably the best overall implementation -- we'll
// likely make this the default in future releases.
unsigned t = current->_hashStateX;
t ^= (t << 11);
current->_hashStateX = current->_hashStateY;
current->_hashStateY = current->_hashStateZ;
current->_hashStateZ = current->_hashStateW;
unsigned v = current->_hashStateW;
v = (v ^ (v >> 19)) ^ (t ^ (t >> 8));
current->_hashStateW = v;
value = v;
}
value &= markWord::hash_mask;
if (value == 0) value = 0xBAD;
assert(value != markWord::no_hash, "invariant");
return value;
}
复制代码
若是没有 C++ 基础的话,不用细致去看每一行代码,咱们只经过表面去了解一下 get_next_hash()
这个方法就行。其中的 hashCode
变量是 JVM 启动时的一个全局参数,能够经过它来切换哈希值的生成策略。
hashCode==0
,调用操做系统 OS 的 random()
方法返回随机数。hashCode == 1
,在 STW(stop-the-world)操做中,这种策略一般用于同步方案中。利用对象地址进行计算,使用不常常更新的随机数(GVars.stw_random
)参与其中。hashCode == 2
,使用返回 1,用于某些状况下的测试。hashCode == 3
,从 0 开始计算哈希值,不是线程安全的,多个线程可能会获得相同的哈希值。hashCode == 4
,与建立对象的内存位置有关,原样输出。hashCode == 5
,默认值,支持多线程,使用了 Marsaglia 的 xor-shift 算法产生伪随机数。所谓的 xor-shift 算法,简单来讲,看起来就是一个移位寄存器,每次移入的位由寄存器中若干位取异或生成。所谓的伪随机数,不是彻底随机的,可是真随机生成比较困难,因此只要能经过必定的随机数统计检测,就能够看成真随机数来使用。至于更深层次的挖掘,涉及到数学知识和物理知识,就不展开了。毕竟菜是原罪。
二哥在 CSDN 上写了不少 Java 方面的系列文章,有 Java 核心语法、Java 集合框架、Java IO、Java 并发编程、Java 虚拟机等,也算是体系完整了。
为了能帮助到更多的 Java 初学者,二哥把本身连载的《教妹学Java》开源到了 GitHub,尽管只整理了 50 篇,发现字数已经来到了 10 万+,内容更是没得说,通俗易懂、风趣幽默、图文并茂。
GitHub 开源地址(欢迎 star):github.com/itwanger/jm…
若是有帮助的话,还请给二哥点个赞,这将是我继续分享下去的最强动力!