zoukankan      html  css  js  c++  java
  • HashMap是怎样存储和快速查找的

    参考:廖雪峰老师的java教程
    我们都知道Map是一种键值对映射表,可以通过key快速查找对应的value.

    以HashMap为例,观察下面的代码:

            Map<String ,Integer> map = new HashMap<>();
            map.put("apple",12);
            map.put("pear",10);
            map.put("origin",5);
            map.get("apple");  //12
    

    HashMap之所以能根据key直接拿到value,,原因是它内部通过空间换时间的方法,用一个大数组存储所有的value,并根据key直接计算出value应该存储在那个索引:

    ┌───┐
    0 │ │
    ├───┤
    1 │ ●─┼───> Person("Xiao Ming")
    ├───┤
    2 │ │
    ├───┤
    3 │ │
    ├───┤
    4 │ │
    ├───┤
    5 │ ●─┼───> Person("Xiao Hong")
    ├───┤
    6 │ ●─┼───> Person("Xiao Jun")
    ├───┤
    7 │ │
    └───┘
    如果key的值为"a",计算得到的索引总为 1 ,因此返回value为Person("Xiao Ming"),如果key的值为 "b",计算得到的索引总为5,因此返回value为Person("Xiao Hong"),这样就不必遍历整个数组,即可以直接读取key对应的value.

    当我们使用 key 存取value的时候,就会引起一个问题:

    我们放入map的可以是字符串 "a",但是,当我们获取Map的value时,不一定就是放入的那个key对象.

    换句话讲,两个key应该是内容相同,但不一定是同一个对象.

    @Test
        public void testHashMap(){
            String key1 = "a";
            Map<String,Integer> map = new HashMap<>();
            map.put(key1,123);
            String key2 = new String("a");
            int i = map.get(key2);
            System.out.println(i);  //123
            System.out.println(key1 == key2); // false  说明key1和key2是两个对象
            System.out.println(key1.equals(key2));  //true    说明key1的内容和key2相同
        }
    

    因为在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内部使用了数组,通过计算key的HashCode()直接定位value所在的索引,那么第一个问题就来了:hashCode()返回的int返回高达±21亿,先不考虑负数,HashMap内部使用的数组得有多大?

    实际上 HashMap初始化默认的数组大小只有 16,任何key,无论它的hashCode()有多大,都可以简单地通过:

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

    把索引确定为0~15之间,即永远不会超出数组范围,上述算法只是一种最简单的实现.

    第二个问题:如果添加超过16个key-value到HashMap,数组不够用怎么办?

    添加超过一定数量的key-value时,HashMap会在内部自动扩容,每次扩容一倍,即长度为16的数组扩展为长度为32,相应的,需要重新计算hashCode() 索引位置.例如:对长度为32的数组计算hashCode()对应的索引,计算方式要改为:

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

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

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

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

    最后一个为题:如果两个不相同的key,例如"a" 和"b",他们的hashCode()恰好是相同的(这种情况是完全有可能的,因为不相等的两个实例,只要求hashCode()尽量不相等),那么,当我们放入:

    map.put("a", new Person("Xiao Ming"));
    map.put("b", new Person("Xiao Hong"));
    

    由于计算出的数组索引相同,后面放入"Xiao Hong"会不会把"Xiao Ming"覆盖了?

    当然不会!使用Map的时候,只要key不相同,他们映射的value就不会互不干扰.但是,在hashMap内部,确实可能存在不同的key,映射到相同的hashCode(),即相同的数组索引上怎么办?

    我们就假设"a" 和"b" 这两个key 最终计算出的索引都是5,那么,在HashMap的数组中,实际存储的不是一个Person实例,而是一个List,它包含 两个Entry,一个是"a"的映射,一个是"b"的映射:

    ┌───┐
    0 │ │
    ├───┤
    1 │ │
    ├───┤
    2 │ │
    ├───┤
    3 │ │
    ├───┤
    4 │ │
    ├───┤
    5 │ ●─┼───> List<Entry<String, Person>>
    ├───┤
    6 │ │
    ├───┤
    7 │ │
    └───┘
    在查找的时候,例如:

    Person p = map.get("a");
    

    HashMap内部通过"a"找到的实际上是List<Entry<String,Person>>,它还需要遍历这个list,并找到一个Entry,它的key字段是"a",才能返回对应的Person实例.

    我们把不同的key具有相同的hashCode()的情况称之为哈希冲突.在冲突的时候,一种最简单的解决办法是用List存储hashCode()相同的key-value.显然冲突的概率越大,这个list就越长,map的get()方法效率就越低,这就是为什么要尽量满足条件二:

    如果两个对象不相等,则两个对象的hashCode()尽量不要相等
    

    hashCode()方法编写得号,HashMap的工作效率就越高.

  • 相关阅读:
    nginx+upsync+consul 构建动态nginx配置系统
    服务容错保护断路器Hystrix之六:缓存功能的使用
    consul之:ACL配置使用
    Consul之:服务健康监测
    Consul实践指导-DNS接口
    Spring 整合Mybatis实例
    ORACLE SEQUENCE 具体解释
    python高速排序
    降阶法计算行列式方法有个地方有Bug(原文也已更正,此为更正后部分)
    MyBatis在Oracle中插入数据并返回主键的问题解决
  • 原文地址:https://www.cnblogs.com/liuzhidao/p/13746648.html
Copyright © 2011-2022 走看看