Java中的hash值主要是用来在散列存储结构中确定对象的存储地址的,提高对象的查询效率。
Java设计的顶级父类Object类中,有两个方法很特殊,它们分别是equals方法与hashCode方法。——一旦重写了equals方法,就一定要重写hashCode方法。以下是Object的源码:
- public class Object {
-
- /*
- * 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.
- */
- public boolean equals(Object obj) {
- return (this == obj);
- }
-
- /*
- * 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.)
- *
- */
- public int hashCode() {
- return identityHashCode(this);
- }
-
-
- /* 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);
- }
-
-
- }
如果一个类没有重写equals(Object obj)方法,则等价于通过==比较两个对象,即比较的是对象在内存中的空间地址是否相等;如果重写了equals(Object obj)方法,则根据重写的方法内容去比较相等,返回true则相等,false则不相等。
equals方法注释中的大致意思是:当我们将equals方法重写后有必要将hashCode方法也重写,这样做才能保证不违背hashCode方法中“相同对象必须有相同哈希值”的约定。
hashCode方法本质就是一个哈希函数,Object类的作者在注释的最后一段的括号中写道:将对象的地址值映为integer类型的哈希值。
提到hashCode,就会想到哈希表,将某一对象的值映射到表中的某个位置,从而达到以O(1)的时间复杂度来查询该对象的值。hashCode是一个native方法,哈希值的计算利用的内存地址。哈希表在一定程度上也可以起到判重的作用,但也可能存储哈希冲突,即使是两个不同的对象,它们的哈希值也可能是相同的。因此,虽然哈希表具有优越的查询性能,但也可能存在哈希冲突。
hashCode方法注释中列了个列表,列表中有三条注释,当前需要理解的大致意思如下:

图中存在两种独立的情况:
相同的对象必然导致相同的哈希值;
不同的哈希值必然是由不同对象导致的;
因此,equals方法与hashCode方法根本就是配套使用的。对于任何一个对象,不论是使用继承自Object的equals方法还是重写equals方法。hashCode方法实际上必须要完成的一件事情就是,为该equals方法认定为相同的对象返回相同的哈希值。如果只重写equals方法没有重写hashCode方法,就会导致``equals`认定相同的对象却拥有不同的哈希值。
Object类中的equals方法区分两个对象的做法是比较地址值,即使用==。如果根据业务需求改写了equals方法的实现,那么也应当同时改写hashCode方法的实现。否则hashCode方法依然返回的是依据Object类中的依据地址值得到的integer哈希值。
代入到具体的例子 –String类,在String类中,equals方法经过重写,具体实现源码如下:
- public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
-
- public boolean equals(Object anObject) {
- if (this == anObject) {
- return true;
- }
- if (anObject instanceof String) {
- String anotherString = (String) anObject;
- int n = length();
- if (n == anotherString.length()) {
- int i = 0;
- while (n-- != 0) {
- if (charAt(i) != anotherString.charAt(i))
- return false;
- i++;
- }
- return true;
- }
- }
- return false;
- }
-
- }
通过源码可以看到,String对象在调用equals方法比较另一个对象时,除了认定相同地址值的两个对象相等以外,还认定对应着的每个字符都相等的两个String对象也相等,即使这两个String对象的地址值不同(即属于两个对象)。
String类中对equals方法进行重写扩充了,但是如果此时我们不将hashCode方法也进行重写,那么String类调用的就是来自顶级父类Obejct类中的hashCode方法。即,对于两个字符串对象,使用它们各自的地址值映射为哈希值。 也就是会出现如下情形:

也就是说,被String类中的equals方法认定为相等的两个对象拥有两个不同的哈希值——因为它们的地址值不同。
为什么重写equals方法就得重写hashCode方法?—— 因为必须保证重写后的equals方法认定相同的两个对象拥有相同的哈希值。同时我们也得出了——hashCode方法的重写原则就是保证equals方法认定为相同的两个对象拥有相同的哈希值。
equals里一般比较的比较全面比较复杂,这样效率就比较低,而利用hashCode进行对比,则只要生成一个hash值进行比较就可以了,效率很高,那么既然hashCode效率这么高为什么还要使用equals进行比较呢?
因为hashCode并不是完全可靠,有时候不同的对象生成的hashcode也会一样(hash冲突),所以hashCode只能说是大部分时候可靠,并不是绝对可靠。 所以可以得出:
equals相等的两个对象,它们的hashCode肯定相等,也就是用equals对比是绝对可靠的;
hashCode相等的两个对象,它们的equals不一定相等,也就是hashCode不是绝对可靠的;
所有对于需要大量并且快速的对比的话如果都用equals去做显然效率太低,解决方式是,每当需要对比的时候, hashCode去对比,这就用到了哈希表,能够快速的地位到对象的存储位置,如果hashCode不一样,则表示这两个对象肯定不相等(也就是不必再用equals去再对比了),如果hashCode相同,此时再对比它们的 equals,如果equals也相同,则表示这两个对象是真的相同了。
查看字符串的hashCode:
- String str = new String("abc");
- System.out.println(str.hashCode()); // 96354
- str += "a";
- System.out.println(str.hashCode()); // 2987071
- str += "b";
- System.out.println(str.hashCode()); // 92599299
- String str1 = "abcab";
- System.out.println(str.hashCode() + " " + str1.hashCode()); // 92599299 92599299
-
- String str2 = "ab";
- String str3 = new String("ab");
- System.out.println(str2.hashCode() + " " + str3.hashCode()); // 3105 3105
以下是String.hashCode的源码:
- public final class String implements java.io.Serializable, Comparable
, CharSequence { -
- private int hash; // Default to 0
-
- /**
- * Returns a hash code for this string. The hash code for a
- * {@code String} object is computed as
- *
- * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
- *
- * using {@code int} arithmetic, where {@code s[i]} is the
- * ith 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;
- if (h == 0 && value.length > 0) {
- char val[] = value;
-
- for (int i = 0; i < value.length; i++) {
- h = 31 * h + val[i];
- }
- hash = h;
- }
- return h;
- }
-
- }
由上面的代码可知,String类的hashCode的值为s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1],其中s是字符串对应的char数组,所以字符串内容一样的String对象,调用hasCode()返回值是一样的。但是反过来,当hashCode返回的值一样时,其字符串内容不一定一样,因为上面的方法计算hash时可能会发生位数溢出。
每个覆盖了equals方法的类中,必须覆盖hashCode。如果不这么做,就违背了hashCode的通用约定,也就是上面注释中所说的。进而导致该类无法结合所以与散列的集合一起正常运作,这里指的是HashMap、HashSet、HashTable、ConcurrentHashMap。
来自 Effective Java 第三版
结论:如果重写equals不重写hashCode它与散列集合无法正常工作。
既然这样那我们就拿我们最熟悉的HashMap来进行演示推导吧。我们知道HashMap中的key是不能重复的,如果重复添加,后添加的会覆盖前面的内容。那么我们看看HashMap是如何来确定key的唯一性的。
- static final int hash(Object key) {
- int h;
- return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
- }
查看代码发现,它是通过计算Map key的hashCode值来确定在链表中的存储位置的。那么这样就可以推测出,如果我们重写了equals但是没重写hashCode,那么可能存在元素重复的矛盾情况。
下面我们来演示一下
- public class Employee {
-
- private String name;
-
- private Integer age;
-
- public Employee(String name, Integer age) {
- this.name = name;
- this.age = age;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Employee employee = (Employee) o;
- return Objects.equals(name, employee.name) &&
- Objects.equals(age, employee.age);
- }
-
- /*@Override
- public int hashCode() {
- return Objects.hash(name, age);
- }*/
- }
- public static void main(String[] args) {
-
- Employee employee1 = new Employee("冰峰", 20);
- Employee employee2 = new Employee("冰峰", 22);
- Employee employee3 = new Employee("冰峰", 20);
-
- HashMap<Employee, Object> map = new HashMap<>();
-
- map.put(employee1, "1");
- map.put(employee2, "1");
- map.put(employee3, "1");
-
- System.out.println("equals:" + employee1.equals(employee3));
- System.out.println("hashCode:" + (employee1.hashCode() == employee3.hashCode()));
- System.out.println(JSONObject.toJSONString(map));
- }
按正常情况来推测,map中值存在两个元素,employee2和employee3。
执行结果

出现这种问题的原因就是因为没有重写hashCode,导致map在计算key的hash值的时候,绝对值相同的对象计算除了不一致的hash值。
接下来我们打开hashCode的注释代码,看看执行结果

如果重写了equals就必须重写hashCode,如果不重写将引起与散列集合(HashMap、HashSet、HashTable、ConcurrentHashMap)的冲突。
重写equals必需重写hashCode。
两个值hashCode值相等,这两不一定相等。
但是两个equals值相等,hashCode一定相等
idea
alt+insert可以生成两个方法,依次改动
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- TextImplicaMarkedDatasetText that = (TextImplicaMarkedDatasetText) o;
- //就是比对是不是相等
- Set<String> set = markedList.stream().map(TextImpMarkedHypot::getText).collect(Collectors.toSet());
- Set<String> set2 = that.markedList.stream().map(TextImpMarkedHypot::getText).collect(Collectors.toSet());
-
- return Objects.equals(text, that.text)
- && OrigDatasetFileHelper.duplicateRemoval(set, set2)
- && OrigDatasetFileHelper.duplicateRemoval(set2, set);
-
- }
-
- @Override
- public int hashCode() {
- int result = text == null ? 0 : text.hashCode();
- if (CollUtil.isNotEmpty(markedList)) {
- //逐个取值累加hashCode
- for (TextImpMarkedHypot textImpMarkedHypot : markedList) {
- result = 31 * result + textImpMarkedHypot.getText() == null ? 0 : textImpMarkedHypot.getText().hashCode();
- }
- }
- return result;
- }
-
-
-
-
- public static boolean duplicateRemoval(Set<String> set1, Set<String> set2) {
- for (String s : set1) {
- if (!set2.contains(s)) {
- return false;
- }
- }
- return true;
- }