hashCode和equals方法:自定义HashMap的key时需要注意什么?

使用

当我们需要自定义HashMap这种散列数据结构(HashSet,HashMap,LinkedHashMap,LinkedHashSet)的Key时候:
需要重写hashCode()和equals(Object o)方法

使用方法

public class Key {
    private Integer id;
    public Integer getId(){
        return id;
    }
    public Key(Integer id){
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
       if(o == null || !(o instanceof  Key)){
           return false;
       }else {
           return this.getId().equals(((Key)o).getId());
       }
    }

    @Override
    public int hashCode() {
       int hashCode =  id.hashCode();
       System.out.println(hashCode);
        return hashCode;
    }
}
    public static void main(String[] args) {
        Key k1 = new Key(1);
        Key k2 = new Key(1);

        HashMap<Key,String> hm= new HashMap<Key,String>();
        hm.put(k1,"key with id is 1");

        System.out.println(hm.get(k2));
    }

我们想通过hm.get(k2)来获取得到“key with id is 1”的打印。

为什么

1.HashMap为了查询效率,存放的时候是通过散列表来存储的。所以需要重写hashCode来保证存储的数组下标一致。
2.equals方法是用来保证相等的。

通过散列来存储,何为散列表?

散列思想

正常我们查询是O(n)。
例如:
在长度为n(假设是500)的线性表(Arraylist)存放着无序的数字。
如果我们要找到一个指定的数字,就不得不通过从头到位一次遍历来查找,这样的平均查找次数是n除以2(250)

散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,如果没有数组,就没有散列表。

利用数组支持根据下标随机访问的时候,时间复杂度是O(1)。
例如我们生活中的一个场景:
学校需要点名,学校3年2班的学号25定义为:030225,这个人是张三。
如果我们想要找到张三,我们只需大喊一声030225,则张三就会喊一声到!
我们很快就可以找到张三这个人。
030225的编号我们叫作键(key)或者关键字
张三我们称作为散列值
030225转化为数组下标的映射方法就叫作散列函数
散列函数在散列表中起着非常关键的作用。正常情况下散列函数的计算是比较复杂的。

我们看稍微复杂一点:
存放元素和存放位置是用hash来关联的。hash函数是x*x%5
hashCode和equals方法:自定义HashMap的key时需要注意什么?_第1张图片
6这个元素会放到索引为1的位置。7的hash函数结果为4。
我们想要从散列表中找6这个元素。我们可以先对元素进行一次hash运算。然后再从结果中找到这个位置。

如果再放入8的话,就会产生Hash冲突。
hashCode和equals方法:自定义HashMap的key时需要注意什么?_第2张图片
hash冲突通过一个链来将7和8关联起来。这个方法就是链地址法。

java中Object类会有HashCode方法。这个HashCode方法就是我们所说的散列函数。HashCode返回的值就是散列值。

我们再回头看上面的结论:
1.HashMap为了查询效率,存放的时候是通过散列表来存储的。所以需要重写hashCode来保证存储的数组下标一致。
就明白了。

equals方法是用来保证相等的

Object.equals方法只是比较的对象的地址。

再次看一开始的例子:
```java
public class Key {
    private Integer id;
    public Integer getId(){
        return id;
    }
    public Key(Integer id){
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
       if(o == null || !(o instanceof  Key)){
           return false;
       }else {
           return this.getId().equals(((Key)o).getId());
       }
    }

    @Override
    public int hashCode() {
       int hashCode =  id.hashCode();
        return hashCode;
    }
}
    public static void main(String[] args) {
        Key k1 = new Key(1);
        Key k2 = new Key(1);

        HashMap<Key,String> hm= new HashMap<Key,String>();
        hm.put(k1,"key with id is 1");

        System.out.println(hm.get(k2));
    }

没有重写hahcode方法的时候,此调用是不对的调用结果是null。
原因:
当我们向hashcode里面放k1的时候,首先会调用Key这个类的HashCode方法来计算它的Hash值,随后把这个值放入这个值所索引的内存的位置。

没有重新定义HashCode的方法,它就会调用Object类的Hashcode方法。而Object里面返回的Hash值,是Key1的内存地址。

k2也是如此,但是k1和k2的内存地址是不一样的。

如果我们用k2去拿

hm.get(k2)

自然拿不到k1的值。

如果重写了hash值,则调用的hashCode是一样的。但是比较两个数是否一样,光hashCode值一样是不行的。并不保证k1和k2真正的相等。还需要重写equals方法。因为hashcode相等的时候,我们仅仅保证hashmap的数组下标相等。hash过程中会有散列冲突。真正保证相等的是equals方法。

如果不重写equals方法,它会调用equals方法的内存地址是否是一样。由于k1和k2是new出来的。他们的内存地址是绝对不一样的。

如何重写?

重写hashcode方法时候,我们判断定义可以就是如果两个对象一样,他们的id就是一样的。
equals不为空或者是Object才来判断内存地址。

    @Override
    public boolean equals(Object o) {
       if(o == null || !(o instanceof  Key)){
           return false;
       }else {
           return this.getId().equals(((Key)o).getId());
       }
    }

    @Override
    public int hashCode() {
       int hashCode =  id.hashCode();
        return hashCode;
    }

再次总结:

我们终于知道了这个使用:

当我们需要自定义HashMap这种散列数据结构(HashSet,HashMap,LinkedHashMap,LinkedHashSet)的Key时候:
需要重写hashCode()和equals(Object o)方法

参考:

《Java编程思想》 17.9散列与散列码

你可能感兴趣的:(java)