HashMap的一些简单笔记

110 阅读6分钟

HasMap的属性

先看一下HashMap的继承体系

hashMap1.png

它继承自抽象类AbstractMap,实现了Map,Cloneable、Serializable接口。

再看一下HashMap的成员变量和一些默认值:

// 默认的初始化数组大小,必须是2的倍数
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//  HashMap的最大长度
static final int MAXIMUM_CAPACITY = 1 << 30;
// 负载因子,默认值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表转树的阈值,当链表长度 > 该值时,链表转为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 链表还原阈值:即 红黑树转为链表的阈值,当在扩容(resize())时(此时HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量 < 6时,则将 红黑树转换成链表
static final int UNTREEIFY_THRESHOLD = 6;
// 最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表 (即 将链表 转换成红黑树);为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;

HashMap的重要的内部类

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        // 省略部分代码...
    }

Node实现了Map的内部接口Map.Entry<k,v> , 它有四个属性 hash,key,value,next,是HashMap内数组每个位置上真正存放元素的数据结构。

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }

TreeNode实现了LinkedHashMap的内部接口LinkedHashMap.Entry<K,V>,它是红黑树节点的具体实现。

    final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }
        public final boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            Node<K,V> candidate = getNode(hash(key), key);
            return candidate != null && candidate.equals(e);
        }
        // 省略部分代码...
    }

EntrySet继承了AbstractSet,它内部有个迭代器iterator,可以获取Map.Entry对象,方法contains用来判断所给的对象是否包含在当前EntrySet中。

put方法源码

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 如果数组是null或者数组为空,就调用resize()进行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // (n-1)&hash 算出下标, 如果当前计算出来的位置为null,就新建一个节点
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 若计算出来的位置上不为null,它和传入的key相比,hashCode相等并且key也相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 那么将p赋给e
                e = p;
            // 如果p是树类型,则按照红黑树的结构存入进去
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTre;
            else {
            	 // 遍历p,p是链表
                for (int binCount = 0; ; ++binCount) {
                	// 如果p的下一个节点是尾节点(尾节点.next=null)
                    if ((e = p.next) == null) {
                    	// 在p的后面创建一个节点,存放key/value(尾插法,多线程并发不会形成循环链表)
                        p.next = newNode(hash, key, value, null);
                        // TREEIFY_THRESHOLD = 8,即当binCount达到7时转换成红黑树数据结构,
						// 因为binCount是从0开始的,达到7时p链表上就有8个节点了,所以是链表上达到8个节点时会转变成红黑树。
                        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;
                    // 若上面两个条件都不满足,此时e = p.next,也就是将p的下一个节点赋给p,进入下一次循环
                    p = e;
                }
            }
            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;
    }

putVal()方法的大致流程逻辑如下:

hashMap2.png

get()方法源码

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // tab不为空,并且当前下标位置的Node节点存在
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 对比第一个node节点的key值,如果是我们要找的值就返回
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 第一个节点不是我们要找的值,将e的值指向当前first节点的下一个值
            if ((e = first.next) != null) {
            	// 如果这是一个红黑树结构,就使用数的方法遍历返回Value
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 仍为链表结构,比较hash值和key值,相等就是我们要的节点,并返回,否则继续遍历下一个节点
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

get()方法代码清晰,逻辑简单,很容易理解。

resize()方法

该方法主要是对HashMap进行扩容操作。

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        // 如果原数组长度大于0
        if (oldCap > 0) {
        	// 原数组的 length 大于 最大数组长度,就不进行扩容操作,直接返回
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 扩容2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];	//创建新数组,容量为原数组的两倍
        // 将table变量赋值为新数组
        table = newTab;
        // 原数组不为 null ,将原数组的 Node 赋值给新数组
        if (oldTab != null) {
        	// 遍历原数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //将不为null的j位置的元素指向e节点
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //若e是尾节点,或者说e后面没有节点了,就将e指向新数组的e.hash&(newCap-1)位置
                    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 {
                            next = e.next;
                            // e.hash & oldCap 的值要么是0要么是oldCap
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;	 // 第一次进来,先确定头节点,以后都走else,loHead指向e
                                else
                                	// 第二次进来时loTail的next指向e(e=e.next),
                    // 注意此时loHead的地址和loTail还是一样的,所以loHead也指向e,
                    // 也就是说e被挂在了loHead的后面(尾插法,不会形成循环链表),
                    // 以此类推,后面遍历的e都会被挂在loHead的后面。
                                    loTail.next = e;
                                 // loTail指向e,第一次进来时头和尾在内存中的指向是一样的都是e,
                    // 第二次进来时,loTail指向了e(e=e.next),这时和loHead.next指向的对象是一样的,
                  // 所以下一次进来的时候loHead可以找到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;
                            // 将loHead节点存到新数组中原下标位置
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            // 将hiHead节点存到新数组中 [原下标+原数组长度] 的位置
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

为什么(e.hash&oldCap) == 0为true或false就能判断存放的位置是newTab[原下标],还是newTab[原下标+原数组长度]?

hashMap3.jpg

并发访问HashMap会出现哪些问题,如何解决呢?

我们知道jdk1.8已经不会在多线程下出现循环链表问题了,那还会出现哪些问题呢?如:数据丢失、结果不一致......。解决方案:

1)HashTable 用synchronized锁住整个table,效率太低,不好。

2)Collections.SynchronizedMap() 它是对put等方法用synchronized加锁的,效率一般是不如ConcurrentHashMap的,用的不多。

3)ConcurrentHashMap 采用锁分段,segment,每次对要操作的那部分数据加锁,并且get()是不用加锁的,这效率就高多了。

为什么最大扩容数量是 1 << 30 ?

  1. HashMap在确定数组下标Index的时候,采用的是( length-1) & hash的方式,只有当length为2的指数幂的时候才能较均匀的分布元素。所以HashMap规定了其容量必须是2的n次方

  2. 由于HashMap规定了其容量是2的n次方,所以我们采用位运算<<来控制HashMap的大小。使用位运算同时还提高了Java的处理速度。HashMap内部由数组构成,Java的数组下标是由Int表示的。所以对于HashMap来说其最大的容量应该是不超过int最大值的一个2的指数幂,而最接近int最大值的2个指数幂用位运算符表示就是 1 << 30

参考资料

www.cnblogs.com/-Marksman/p…