[Java]Java中哈希表之HashMap的常见用法及原理

Java中哈希表之HashMap的常见用法及原理

(参考:https://blog.csdn.net/visant/article/details/80045154 )

一、HashMap介绍

基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。

二、HashMap原理

1. 存储原理

可以看作是数组和链表结合组成的复合结构,数组被分为一个个桶(bucket),每个桶存储有一个或多个Entry对象(每个Entry对象包含三部分key、value,next),通过哈希值决定了Entry对象(键值对)在这个数组的寻址;哈希值相同的Entry对象(键值对),则以链表形式存储。如果链表大小超过树形转换的阈值(TREEIFY_THRESHOLD= 8),链表就会被改造为树形结构。
例如: 第一个键值对A进来。通过计算其key的hash得到的index=0。记做:Entry[0] = A。
第二个键值对B,通过计算其index也等于0, HashMap会将B.next =A,Entry[0] =B,
第三个键值对 C,index也等于0,那么C.next = B,Entry[0] = C;
这样我们发现index=0的地方事实上存取了A,B,C三个键值对,它们通过next这个属性链接在一起。 对于不同的元素,可能计算出了相同的函数值,这样就产生了“冲突”,这就需要解决冲突,“直接定址”与“解决冲突”是哈希表的两大特点。

2. 工作原理

HashMap的工作原理 :HashMap是基于散列法(又称哈希法)的原理,使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket(桶)位置来储存Entry对象。HashMap是在bucket中储存键对象和值对象,作为Map.Entry,并不是仅仅只在bucket中存储值。

3. 存取过程

3.1 put键值对存数据

在这里插入图片描述

  • ①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
  • ②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
  • ③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
  • ④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
  • ⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
  • ⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

3.1 get获取值
  • ① 指定key 通过hash函数得到key的hash值 int hash=key.hashCode();
  • ② 调用内部方法getNode(),得到桶号(一般为hash值对桶数求模) int index = hash%Entry[].length;jdk1.6版本后使用位运算替代模运算,int index=hash&( Entry[].length - 1)
  • ③ 比较桶的内部元素是否与key相等,若都不相等,则没有找到。相等,则取出相等记录的value。
  • ④ 如果得到 key所在的桶的头结点恰好是红黑树节点,就调用红黑树节点的 getTreeNode() 方法,否则就遍历链表节点。getTreeNode方法使通过调用树形节点的 find()方法进行查找。由于之前添加时已经保证这个树是有序的,因此查找时基本就是折半查找,效率很高。
  • ⑤ 如果对比节点的哈希值和要查找的哈希值相等,就会判断 key 是否相等,相等就直接返回;不相等就从子树中递归查找。

4. 查询时间复杂度

HashMap的本质可以认为是一个数组,数组的每个索引被称为桶,每个桶里放着一个单链表,一个节点连着一个节点。很明显通过下标来检索数组元素时间复杂度为O(1),而且遍历链表的时间复杂度是O(n),所以在链表长度尽可能短的前提下,HashMap的查询复杂度接近O(1)

三、HashMap使用方法

1. 构造方法摘要

  • HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
  • HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
  • HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap。
  • HashMap(Map<? extends K,? extends V> m):构造一个映射关系与指定 Map 相同的 HashMap。

2. 方法摘要

  • void clear():从此映射中移除所有映射关系。
  • Object clone():返回此 HashMap实例的浅表复制:并不克隆键和值本身。
  • boolean containsKey(Object key):如果此映射包含对于指定的键的映射关系,则返回 true。
  • boolean containsValue(Object value): 如果此映射将一个或多个键映射到指定值,则返回 true。
  • Set<Map.Entry<K,V>> entrySet():返回此映射所包含的映射关系的 collection 视图。 V get(Object key):返回指定键在此标识哈希映射中所映射的值,如果对于此键来说,映射不包含任何映射关系,则返回 null。
  • boolean isEmpty():如果此映射不包含键-值映射关系,则返回 true。 Set keySet():返回此映射中所包含的键的set 视图。
  • V put(K key, V value): 在此映射中关联指定值与指定键。
  • void putAll(Map<?extends K,? extends V>m):将指定映射的所有映射关系复制到此映射中,这些映射关系将替换此映射目前针对指定映射的所有键的所有映射关系。
  • V remove(Object key): 如果此映射中存在该键的映射关系,则将其删除。
  • int size():返回此映射中的键-值映射关系数。
  • Collection values():返回此映射所包含的值的collection 视图。

3. 排序

3.1 HashMap按Value排序
/* HashMap按Value排序 */
   public static List<Map.Entry<String, Integer>> mapValueSort(HashMap<String, Integer> labelsMap) {
       List<Map.Entry<String, Integer>> list = new ArrayList<>(labelsMap.entrySet());
       list.sort(new Comparator<Map.Entry<String, Integer>>() {
           public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
               /* compare返回值为三种:(A为前者,B为后者)
               * -1:A<B
               * 0:A=B
               * 1:A>B
               * */
               return o1.getValue() < o2.getValue() ? 1 : ((o1.getValue() == o2.getValue()) ? 0 : -1);
           }
       });
       for (Map.Entry<String, Integer> mapping : list) {
           System.out.println(mapping.getKey() + ":" + mapping.getValue());
       }
       return list;
   }
3.2 HashMap按Value排序
    /* HashMap按Value排序 */
    public static List<Map.Entry<String, Integer>> mapKeySort(HashMap<String, Integer> labelsMap) {
        List<Map.Entry<String, Integer>> list = new ArrayList<>(labelsMap.entrySet());
        list.sort(new Comparator<Map.Entry<String, Integer>>() {
            public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
                /* compareTo返回值:
                   如果此字符串小于字符串参数,则返回一个小于 0 的值
                   如果参数字符串等于此字符串,则返回值 0
                   如果此字符串大于字符串参数,则返回一个大于 0 的值*/
                return o1.getKey().compareTo(o2.getKey());
            }
        });
        for (Map.Entry<String, Integer> mapping : list) {
            System.out.println(mapping.getKey() + ":" + mapping.getValue());
        }
        return list;
    }
3.3 HashMap按Value排序 如果Value相同按Key的字典序排序
    /* HashMap按Value排序 如果Value相同按Key的字典序排序 */
    public static List<Map.Entry<String, Integer>> mapKeyAndValueSort(HashMap<String, Integer> labelsMap) {
        List<Map.Entry<String, Integer>> list = new ArrayList<>(labelsMap.entrySet());
        list.sort(new Comparator<Map.Entry<String, Integer>>() {
            public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
                /* compareTo返回值:
                   如果此字符串小于字符串参数,则返回一个小于 0 的值
                   如果参数字符串等于此字符串,则返回值 0
                   如果此字符串大于字符串参数,则返回一个大于 0 的值*/
                if(o1.getValue() == o2.getValue()){
                    return o1.getKey().compareTo(o2.getKey());
                }else{
                    return o2.getValue().compareTo(o1.getValue());
                }
            }
        });
        for (Map.Entry<String, Integer> mapping : list) {
            System.out.println(mapping.getKey() + ":" + mapping.getValue());
        }
        return list;
    }

4.输出

HashMap<String, Integer> maps = new HashMap<>();
4.1 entrySet() 返回此映射中包含的映射关系的set视图
		/* 遍历Map */
        for (Map.Entry<String, Integer> mapping : maps.entrySet()) {
            System.out.println(mapping.getKey() + ":" + mapping.getValue());
        }
4.2 获取指定key对应的value
		System.out.println(maps.get("2"));
4.3 maps.keySet()
        //得到map的key的集合
        Set<String> keySet = maps.keySet();
        //得到map的key的集合后,循环的根据key去取value就好了
        for (String key : keySet) {
            Integer value = maps.get(key);
            System.out.println(key+":"+value);
        }

四、其他说明

1. 如何重新调整HashMap的大小,如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

HashMap的扩容阈值(threshold = capacity* loadFactor 容量范围是16~2的30次方),就是通过它和size进行比较来判断是否需要扩容。默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组(jdk1.6,但不超过最大容量),来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

2. 解决 hash 冲突的常见方法

a. 链地址法:将哈希表的每个单元作为链表的头结点,所有哈希地址为 i 的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。
b. 开放定址法:即发生冲突时,去寻找下一个空的哈希地址。只要哈希表足够大,总能找到空的哈希地址。
c. 再哈希法:即发生冲突时,由其他的函数再计算一次哈希值。
d. 建立公共溢出区:将哈希表分为基本表和溢出表,发生冲突时,将冲突的元素放入溢出表。
HashMap 就是使用链地址法来解决冲突的(jdk8中采用平衡树来替代链表存储冲突的元素,但hash() 方法原理相同)。当两个对象的hashcode相同时,它们的bucket位置相同,碰撞就会发生。此时,可以将 put 进来的 K- V 对象插入到链表的尾部。对于储存在同一个bucket位置的链表对象,可通过键对象的equals()方法用来找到键值对。

3. 对比:Hashtable、HashMap、TreeMap

Hashtable 是早期Java类库提供的一个哈希表实现,本身是同步的,不支持 null 键和值,由于同步导致的性能开销,所以已经很少被推荐使用。
HashMap与 HashTable主要区别在于 HashMap 不是同步的,支持 null 键和值等。通常情况下,HashMap 进行 put 或者 get 操作,可以达到常数时间的性能,所以它是绝大部分利用键值对存取场景的首选。
TreeMap 则是基于红黑树的一种提供顺序访问的 Map,和 HashMap 不同,它的 get、put、remove 之类操作都是 O(log(n))的时间复杂度,具体顺序可以由指定的 Comparator 来决定,或者根据键的自然顺序来判断。

五、源码

https://github.com/zhangzhishun/java/blob/master/src/untils/_Map.java

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一杯糖不加咖啡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值