行动是治愈恐惧的良药,而犹豫、拖延将不断滋养恐惧。

1.Hash概述

Hash称为“散列”或者音译“哈希”,本质上是一种压缩映射,也就是说把任意长度的消息,通过散列算法压缩成某一固定长度的消息,由于散列的空间通常远小于输入的空间,因此不同的输入可能会散列出相同的输出。

2.hashCode的作用

hashCode方法返回一个hash code值,这个方法是为了更好的支持hash表,例如:HashMap、HashTable等,
在使用hash表这类数据结构时,首先需要通过hash函数计算出一个hash值,然后根据这hash值,放入到对应的Hash表对应的Key上,如果不同的对象确产生了相同的hash值,也就是发生了Hash值相同导致冲突的情况,那么就在这个Hash key的地方产生一个链表,将所有产生相同hash值的对象放到这个单链表上去,串在一起。

Java基础深度总结:Object类-hashCode方法_数组

查找某个对象时,首先会计算出要查询对象的hash值,通过hash值确定该对象在哪个链表中,然后用equals方法依次判断该对象和链表上的对象是否相等。

  • 提高了对象的查询效率,只有在发生了hash冲突的情况下,才需要通过线性查找对象。
  • 如果两个对象相同,那么它们的hash值一定相同;如果两个对象的hash值相同,并不一定表示两个对象相同,但他们一定在同一个链表上。

Tips:

  • hashCode方法返回一个32位的int整数,hash值的取值范围是: -2^31 ~ 2^31 -1 ,因此若存在 2^32 + 1个对象,那么一定会有两个不同的对象有一样的hash值。
  • Object.hashCode返回的hash值不是对象内存地址。

3.为什么重写equals的同时还得重写hashCode(重点)

因为如果你只重写了equals方法而没有重写hashCode方法,那么在使用散列这样的数据结构时,没办法处理好你的键。这事实上违反了Object hashCode通约的第二条。

java.lnag.Object中对hashCode通约:

  1. 在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话,则对该对象调用hashCode方法多次,它必须始终如一地返回同一个整数。
  2. 如果两个对象根据equals(Object o)方法是相等的,则调用这两个对象中任一对象的hashCode方法必须产生相同的整数结果。
  3. 如果两个对象根据equals(Object o)方法是不相等的,则调用这两个对象中任一个对象的hashCode方法,不要求产生不同的整数结果。但如果能不同,则可能提高散列表的性能。

下面举出了一个只重写了equals方法的例子:

import java.util.HashMap;
import java.util.Map;

public class EqualsAndHashCodeTest {

    public static void main(String[] args) {

        Map<Key, Integer> map = new HashMap<Key, Integer>();
        Key k1 = new Key("A");
        Key k2 = new Key("A");
        map.put(k1, 2);
        System.out.println("map.get(k1):" + map.get(k1));
        System.out.println("map.get(k2):" + map.get(k2));
        System.out.println("k1.equals(k2):" + k1.equals(k2));
        System.out.println("k1.hashCode:" + k1.hashCode());
        System.out.println("k2.hashCode:" + k2.hashCode());
    }

}

class Key {
    private String k;

    public Key(String key) {
        this.k = key;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        
        Key key = (Key) o;
        
        return k != null ? k.equals(key.k) : key.k == null;
    }
    
}
输出:
map.get(k1):2
map.get(k2):null
k1.equals(k2):true
k1.hashCode:21685669
k2.hashCode:2133927002

解析:
可以看到,k1和k2是等价的,但map.get(k2)并没有拿到我们想要取得的对象,因为Key并没有去重写hashCode方法,所以k1、k2的hash值是不一样的,所以以k2的hash值是无法正确找到k1 hash值所对应值的。这也说明了,如果你只实现equals方法而没有实现hashCode方法,那么将会给你在使用散列这类数据结构时埋下隐患!

4.hashCode的实现

1.把某个非0的常数值,比如17,保存在一个名为result的int类型的变量中。

2.对于对象中的每个域,做如下操作:

    a.为该域计算int类型的哈希值c:

  • 如果该域是boolean类型,则计算(f?1:0)
  • 如果该域是byte、char、short或者int类型,则计算(int)f
  • 如果该域是long类型,则计算(int)(f^(f>>>32))
  • 如果该域是float类型,则计算Float.floatToIntBits(f)
  • 如果该域是double类型,则计算Double.doubleToLongBits(f),然后重复第三个步骤。
  • 如果该域是一个对象引用,并且该类的equals方法通过递归调用equals方法来比较这个域,同样为这个域递归的调用hashCode,如果这个域为null,则返回0。
  • 如果该域是数组,则要把每一个元素当作单独的域来处理,递归的运用上述规则,如果数组域中的每个元素都很重要,那么可以使用Arrays.hashCode方法。

    b.把上面计算得到的hash值c 按照result = 31*result + c 公式 加入到result中

3.返回result

示例代码:

import java.util.Arrays;

public class HashCodeExample {

    private boolean flag;
    private int num;
    private long longNum;
    private double doubleNum;
    private int[] ids;
    private Student student;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        HashCodeExample that = (HashCodeExample) o;

        if (flag != that.flag) return false;
        if (num != that.num) return false;
        if (longNum != that.longNum) return false;
        if (Double.compare(that.doubleNum, doubleNum) != 0) return false;
        if (!Arrays.equals(ids, that.ids)) return false;
        return student != null ? student.equals(that.student) : that.student == null;
    }

    @Override
    public int hashCode() {
        int result;
        long temp;
        //boolean
        result = (flag ? 1 : 0);
        //int
        result = 31 * result + num;
        //long
        result = 31 * result + (int) (longNum ^ (longNum >>> 32));
        //double
        temp = Double.doubleToLongBits(doubleNum);
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        //数组
        result = 31 * result + Arrays.hashCode(ids);
        //引用
        result = 31 * result + (student != null ? student.hashCode() : 0);
        return result;
    }
}

class Student{
    private int id;
    private String name;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        if (id != student.id) return false;
        return name != null ? name.equals(student.name) : student.name == null;
    }

    @Override
    public int hashCode() {
        int result = id;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }
}

Tips:

  • 计算hash值时必须排除equals比较时没有用到的域,否则可能会违反Object.hashCode通约的第二条。
  • 31是个奇素数,而且31的乘法可以用移位和减法替代:31* i == ( i << 5 ) - i ,JVM会自动优化的。
  • hashCode和equals方法都可以用开发工具的快捷键生成的。