算法与数据结构 (八) HashMap源码

一  存储结构

static class Node<K,V> implements Map.Entry<K,V> {    final int hash;    final K key;    V value;    Node<K,V> next;}
transient Node<K,V>[] table;
内部存储的单元如上所示,整体上就是数组加链表的桶状结构。

二  put操作

put(key,value)内部调用的是putVal() 下面是源码  jdk1.8采用的是尾插法

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;     //如果是第一个元素 就初始化数组  懒加载方式
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);  // 如果当前位置 还没有元素 就初始化当前的位置  这里确定位置的方法 是&运算
        else {   //查找节点(键值为key)
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }              //以下代码好像是为了linkedhashmap服务的
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;// 如果容量超了 就扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

  代码除了红黑树的部分引出两个部分:1. 为什么采取hash & 长度-1 的方式找数组位置  2.  如何扩容

三  初始化和扩容

定容量的方法:

static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

  对于构造函数中传入的整数,进行如下操作。结论就是得到的数,是大于等于传入值的最小 2 的幂。也就是传入64 就是64 ,传入127 就是 128。

这算法想起来不好想,但看代码还是好理解。对于65这种,减去1,就是 100 0000  第一次运算 是110 0000 第二次: 111 1000,结果变成1111111(全1) 最后加上1  必然是2 的整次幂。

这也解决了 hash运算为什么用   hash & n-1,因为&的效率大于除法和取余运算,而且由于是2的整次幂,按位与和取余操作效果一样,但是速度更快。

此外 node节点中的hash 值 ,是这样得到的,也就是原来key hashcode高16位和hashcode亦或。为的是增加高16位的参与感,减少hash冲突,如果key的hashcode 是  0A01 0B01 0C01 如果直接进行 按位与操作,那么他们都被映射到同一个位置,而经过这么一轮操作会,会减少hash碰撞的情况。

static final int hash(Object key) {    int h;    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

而扩容主要是resize方法  主要是这段代码 ,新的数组是旧的数组的二倍,如下是旧的数据迁移到新数组中
if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)       //第一种情况,节点就一个元素
                        newTab[e.hash & (newCap - 1)] = e;  //当前节点在新节点中的位置
                    else if (e instanceof TreeNode)   //第二种情况,节点是红黑树
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order           //第三种情况 节点是链表
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {    //分成两类 比如旧容量是16  对于 hash 17 1 他们都在下标为1的位置,但是 &16 ,要么是0 要么是1 。这就是分类的标准  为0 的放在新的数组的1的位置,为1的放到17 的位置
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }

  四 总结

1 .hash值的算法,键的hash值与 高16亦或,保留高位信息

---映射下标值 &(容量-1)得到实际的

2.  保证容量是2的幂 采取按位或

3. .hashMap的结构

在1.8做出的改变,数组加红黑树 链表大于8就会变树,

变树的代价比较高,所以还增加了一个条件,就是数组大小要大于64,否则采取扩容的方法,解决链表过长的问题。

4.hashmap可以插入空值键

有单独的方法,永远插在第一个链表,也就是数组下标为0的位置

5.遍历方法

map.entrySet()   这是拿到了所有的键值对

keyset()    这个是拿到了所有的键值

6.并发下的问题

put()丢失更新

resize()也可能丢失更新

原文地址:https://www.cnblogs.com/caijiwdq/p/11072465.html

时间: 2024-11-03 08:17:00

算法与数据结构 (八) HashMap源码的相关文章

HashMap源码阅读(1)- 初始值、数据结构、hash计算

最近有被问及HashMap的相关问题,不得不再阅读源码,刨根问底. 1)初始值 我们平常使用Map的时候,创建的时候都是Map<String,Object> map = new HashMap<String,Object>();那么HashMap的默认大小是多少呢?查看源码,发现这么一段: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /** * The default initial capacity - MUST be a power of

HashMap源码中一个算法tableSizeFor

阅读JDK1.8版本HashMap源码看到的一段代码,返回大于等于指定入参的最小2的幂. 1 /** 2 * The maximum capacity, used if a higher value is implicitly specified 3 * by either of the constructors with arguments. 4 * MUST be a power of two <= 1<<30. 5 */ 6 static final int MAXIMUM_CAP

HashMap源码分析(转载)

一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同.)此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap. Map map = Coll

HashMap源码分析(JDK1.8)- 你该知道的都在这里了

我的csdn博客地址: http://blog.csdn.net/brycegao321 HashMap是Java和Android程序员的基本功, JDK1.8对HashMap进行了优化, 你真正理解它了吗? 考虑如下问题: 1.哈希基本原理?(答:散列表.hash碰撞.链表.红黑树) 2.hashmap查询的时间复杂度, 影响因素和原理? (答:最好O(1),最差O(n), 如果是红黑O(logN)) 3.resize如何实现的, 记住已经没有rehash了!!!(答:拉链entry根据高位b

【转】Java HashMap 源码解析(好文章)

- .fluid-width-video-wrapper { width: 100%; position: relative; padding: 0; } .fluid-width-video-wrapper iframe, .fluid-width-video-wrapper object, .fluid-width-video-wrapper embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } [

Java集合:HashMap源码剖析

一.HashMap概述二.HashMap的数据结构三.HashMap源码分析     1.关键属性     2.构造方法     3.存储数据     4.调整大小 5.数据读取                       6.HashMap的性能参数                      7.Fail-Fast机制 一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null

HashMap 源码解析

HashMap简介: HashMap在日常的开发中应用的非常之广泛,它是基于Hash表,实现了Map接口,以键值对(key-value)形式进行数据存储,HashMap在数据结构上使用的是数组+链表.允许null键和null值,不保证键值对的顺序. HashMap检索数据的大致流程: 当我们使用HashMap搜索key所对应的value时,HashMap会根据Hash算法对key进行计算,得到一个key的hash值,再根据hash值算出该key在数组中存储的位置index,然后获取数组在inde

Java中HashMap源码分析

一.HashMap概述 HashMap基于哈希表的Map接口的实现.此实现提供所有可选的映射操作,并允许使用null值和null键.(除了不同步和允许使用null之外,HashMap类与Hashtable大致相同)此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap. Map map = Collections.sync

[转载] Java集合---HashMap源码剖析

转载自http://www.cnblogs.com/ITtangtang/p/3948406.html 一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同.)此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collection

Java进阶之----HashMap源码分析

今天我们接着来看HashMap的源码,对几个常用的方法进行分析.在分析之前,我们还是要先对HashMap的结构有一个了解.看过之前我分析的ArrayList和LinkedList源码的朋友应该清楚,ArrayList内部是以数组实现的,LinkedList内部是以链表实现的.而HashMap则是对数组和链表的结合,虽然看上去复杂了一些,不过仔细分析一下,还是很好理解的.我们来看一张图片,是我根据我的理解画的. 我们在来看看Entry的内部结构是什么: 以上两个图,相信大家对HashMap的结构有