Java笔记之编写hashCode

Hashmap

Map是一种键-值(key-value)映射表,Hashmap就是map的一种,HashMap之所以能根据key直接拿到value,原因是它内部通过空间换时间的方法,用一个大数组存储所有value,并根据key直接计算出value应该存储在哪个索引:
在这里插入图片描述
如果key的值为"a",计算得到的索引总是1,因此返回valuePerson("Xiao Ming"),如果key的值为"b",计算得到的索引总是5,因此返回value为Person("Xiao Hong"),这样,就不必遍历整个数组,即可直接读取key对应的value。
当我们使用key存取value的时候,就会引出一个问题:
我们放入Map的key是字符串"a",但是,当我们获取Map的value时,传入的变量不一定就是放入的那个key对象。
换句话讲,两个key应该是内容相同,但不一定是同一个对象。

import java.util.HashMap;//记得写HashMap的包
import java.util.Map;
public class Main {
    public static void main(String[] args) {
        String key1 = "a";
        Map<String, Integer> map = new HashMap<>();//构建新映射的方法
        map.put(key1, 123);

        String key2 = new String("a");
        map.get(key2); // 123

        System.out.println(key1 == key2); // false因为地址不同
        System.out.println(key1.equals(key2)); // true值是相同的
    }
}

因为在Map的内部,对key做比较是通过equals()实现的,这一点和List查找元素需要正确覆写equals()是一样的,即正确使用Map必须保证:作为key的对象必须正确覆写equals()方法。
我们经常使用String作为key,因为String已经正确覆写了equals()方法。但如果我们放入的key是一个自己写的类,就必须保证正确覆写了equals()方法。我们再看一下HashMap为什么能通过key直接计算出value存储的索引。相同的key对象(使用equals()判断时返回true)必须要计算出相同的索引,否则,相同的key每次取出的value就不一定对。通过key计算索引的方式就是调用key对象的hashCode()方法,它返回一个int整数。HashMap正是通过这个方法直接定位key对应的value的索引,继而直接返回value。(这种方式就像数据结构算法里面的散列查找一样)
因此,正确使用Map必须保证:
①作为key的对象必须正确覆写equals()方法
②相等的两个key实例调用equals()必须返回true
②作为key的对象还必须正确覆写hashCode()方法
hashCode()方法要严格遵循以下规范:
①如果两个对象相等,则两个对象的hashCode()必须相等;
②如果两个对象不相等,则两个对象的hashCode()尽量不要相等。
上述第一条规范是正确性,必须保证实现,否则HashMap不能正常工作。而第二条如果尽量满足,则可以保证查询效率,因为不同的对象,如果返回相同的hashCode(),会造成Map内部存储冲突,使存取的效率下降。
比如我们给person编写hashcode方法

public class Person {
    String firstName;
    String lastName;
    int age;
//这3个字段分别相同的实例,hashCode()返回的int必须相同
    @Override
    int hashCode() {
        int h = 0;
        h = 31 * h + firstName.hashCode();
        h = 31 * h + lastName.hashCode();
        h = 31 * h + age;
        return h;
    }
}

String类已经正确实现了hashCode()方法,我们在计算PersonhashCode()时,反复使用31*h,这样做的目的是为了尽量把不同的Person实例的hashCode()均匀分布到整个int范围(类似散列查找里为了解决冲突的散列表构造方法一样避免产生聚集现象)。和实现equals()方法遇到的问题类似,如果firstNamelastName为null,上述代码工作起来就会抛NullPointerException

为了解决这个问题,我们在计算hashCode()的时候,经常借助Objects.hash()来计算:

int hashCode() {
    return Objects.hash(firstName, lastName, age);
}

编写equals()和hashCode()遵循的原则是:
equals()用到的用于比较的每一个字段,都必须在hashCode()中用于计算,没有使用到的字段,绝不可放在hashCode()中计算。

拓展延伸

既然HashMap内部使用了数组,通过计算keyhashCode()直接定位value所在的索引,那么问题来了:hashCode()返回的int范围高达±21亿,就算不考虑负数,HashMap内部使用的数组得有多大?
实际上HashMap初始化时默认的数组大小只有16,任何key,无论它的hashCode()有多大,都可以简单地通过:

int index = key.hashCode() & 0xf; //16进制   0xf = 15

把索引确定在0~15,即永远不会超出数组范围,上述算法只是一种最简单的实现。
如果添加超过16key-valueHashMap,数组不够用了怎么办?
添加超过一定数量的key-value时,HashMap会在内部自动扩容,每次扩容一倍,即长度为16的数组扩展为长度32,相应地,需要重新确定hashCode()计算的索引位置。例如,对长度为32的数组计算hashCode()对应的索引,计算方式要改为:

int index = key.hashCode() & 0x1f; // 0x1f = 31

由于扩容会导致重新分布已有的key-value,所以,频繁扩容对HashMap的性能影响很大。如果我们确定要使用一个容量为10000key-valueHashMap,更好的方式是创建HashMap时就指定容量:

Map<String, Integer> map = new HashMap<>(10000);

虽然指定容量是10000,但HashMap内部的数组长度总是2n,因此,实际数组长度被初始化为比10000大的16384(214)。

和散列查找一样在编写hashcode方法时要尽量避免产生相同的hash值,那样hashmap在用的时候效率会更高。

发布了85 篇原创文章 · 获赞 10 · 访问量 3696

猜你喜欢

转载自blog.csdn.net/LebronGod/article/details/104821887