HashMap详解

一、概念及概述

HashMap是基于哈希表Map接口非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。HashMap储存的是键值对,因为HashMap是非synchronized,所以HashMap很快,但不保证映射的顺序,特别是它不保证该顺序恒久不变

HashMap 内部结构:可以看作是数组和链表结合组成的复合结构,数组被分为一个个桶,每个桶存储有一个或多个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这个属性链接在一起

由于HashMap是一个散列桶(数组和链表),采用了数组和链表的数据结构,所以能在查询和修改方便继承了数组的线性查找和链表的寻址修改

数组:存储区间连续,占用内存严重,寻址容易,插入删除困难;
链表:存储区间离散,占用内存比较宽松,寻址困难,插入删除容易;
Hashmap综合应用了这两种数据结构,实现了寻址容易,插入删除也容易。
hashMap的结构示意图如下:

 

 

二、HashMap的插入和取出的工作流程

简述:HashMap是基于散列法(又称哈希法)的原理,使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。

1、put(key, value)方法流程图:

PS:根据键值计算出hashCode后得到插入数组得索引 i 得原理是:
       int index =hash%Entry[].length;
      Jdk1.6版本后使用位运算替代模运算:int index=hash&( Entry[].length - 1);

 

2、get(key)方法流程:

1)HashMap使用键对象的hashcode函数得到key的hash值(hashcode)
      int hash=key.hashCode();

2)根据hashcode找到bucket位置(上面PS的计算方法)

3)遍历链表,调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象

4)如果得到 key 所在的桶的头结点是红黑树节点,就调用红黑树节点的 getTreeNode() 方法,否则才执行3

 

 

三、关于HashMap中的碰撞

当两个对象的hashcode相同时,它们的bucket位置相同,‘碰撞’会发生。‘碰撞’发生越多,链表拉的越长,降低效率

String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。

其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。

如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。

如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能

如果采用自定义对象当作HashMap的Key,也要注意上述内容

 

解决 hash 冲突的常见方法

a. 链地址法:将哈希表的每个单元作为链表的头结点,所有哈希地址为 i 的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。

b. 开放定址法:即发生冲突时,去寻找下一个空的哈希地址。只要哈希表足够大,总能找到空的哈希地址。

c. 再哈希法:即发生冲突时,由其他的函数再计算一次哈希值。

d. 建立公共溢出区:将哈希表分为基本表和溢出表,发生冲突时,将冲突的元素放入溢出表。

HashMap 就是使用链地址法(拉链法)来解决冲突的(jdk8中采用平衡树来替代链表存储冲突的元素,但hash() 原理相同)。

 

 

四、关于HashMap的扩容

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

loadFactor:负载因子,默认大小为0.75

capacity:桶(bucket)的个数,HashMap 默认bucket数组大小为16

 // 默认的初始容量(容量为HashMap中槽的数目)是16,且实际容量必须是2的整数次幂。    
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16  Capacity
     // 默认加载因子为0.75   
    static final float DEFAULT_LOAD_FACTOR = 0.75f;  LoadFactor
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    // 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)  
    static final int MAXIMUM_CAPACITY = 1 << 30;

接下来我们看看resize方法是如何将table增加长度的:

    void resize(int newCapacity) {
        // 保存老的table和老table的长度
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        // 创建一个新的table,长度为之前的两倍
        Entry[] newTable = new Entry[newCapacity];
        // hash有关
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        // 这里进行异或运算,一般为true
        boolean rehash = oldAltHashing ^ useAltHashing;
        // 将老table的原有数据,从新存储到新table中
        transfer(newTable, rehash);
        // 使用新table
        table = newTable;
        // 扩容后的HashMap的扩容阀门值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

再来看看transfer方法是如何将把老table的数据,转到扩容后的table中的:

    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        // 遍历老的table数组
        for (Entry<K,V> e : table) {
            // 遍历老table数组中存储每条单项链表
            while(null != e) {
                // 取出老table中每个Entry
                Entry<K,V> next = e.next;
                if (rehash) {
                    //重新计算hash
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                // 根据hash值,算出老table中的Entry应该在新table中存储的index
                int i = indexFor(e.hash, newCapacity);
                // 让老table转移的Entry的next指向新table中它应该存储的位置
                // 即插入到了新table中index处单链表的表头
                e.next = newTable[i];
                // 将老table取出的entry,放入到新table中
                newTable[i] = e;
                // 继续取老talbe的下一个Entry
                e = next;
            }
        }
    }

HashMap是先遍历旧table,再遍历旧table中每个元素的单向链表,取得Entry以后,重新计算hash值,然后存放到新table的对应位置,最后使用新的table,并重新计算HashMap的扩容阀值。

 

PS:如果new HashMap<>(19),bucket数组多大?
HashMap 的 bucket 数组大小一定是2的幂如果 new 的时候指定了容量且不是2的幂, 
实际容量会是最接近(大于)指定容量的2的幂,比如 new HashMap<>(19),比19大且最接近的2的幂是32,实际容量就是32。
源码:

//jdk1.7
private void inflateTable(int toSize) {
     // Find a power of 2 >= toSize,  2的幂 >= toSize
     int capacity = roundUpToPowerOf2(toSize); //计算一定为2的幂

     threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
     table = new Entry[capacity];
     initHashSeedAsNeeded(capacity);
 }

 

 

五、HashMap和CurrentHashMap的区别

说简单点就是HashMap是线程不安全的,单线程情况下使用;而ConcurrentHashMap是线程安全的,多线程使用!

可以使用 Collections.synchronizedMap(new HashMap)

 

六、JDK1.7和JDK1.8中HashMap的实现有哪些区别?

1、红黑树

在JDK1.7版本中.不管负载因子和Hash算法设计的再合理,也免不了会出现拉链(单链表)过长的情况,一旦出现拉链(单链表)过长,会严重影响HashMap的性能。

在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。

2、一些操作方法的优化如resize

resize()用来第一次初始化,或者 put 之后数据超过了threshold(Capacity * LoadFactor)后扩容
jdk1.7 直接扩容两倍,table.length * 2; 源码中使用resize(2 * table.length);
jdk1.8 优化数组下标计算: index = (table.length - 1) & hash ,由于 table.length 也就是capacity 肯定是2的N次方,使用 & 位运算意味着只是多了最高位, 这样就不用重新计算 index,元素要么在原位置,要么在原位置+ oldCapacity

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值