java 7/8 中的HashMap解析

简介
  • HashMap是基于哈希表实现的,每一个元素都有一个key - value。
  • HashMap 存在哈希冲突,在java7之前使用的是数组+链表来解决冲突,在java8之后,对HashMap进行了一些修改,最大的区别就是使用了红黑树,也就是其由数组+链表+红黑树组成的。
  • 在java 7中,在HashMap中查找的时候,根据hash值可以快速定位到数组的下标,但是之后比较链表中的数字的话,就需要一个个比较,这时候时间复杂度就取决于链表的长度o(n)
  • 在java8中,当链表的元素超过8个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为O(logN)。
  • 不管在java7中还是java 8 中,当数组容量不足时,都是需要扩容的
  • HashMap并不支持并发操作,只能用在单线程操作下,如果是在多线程环境中可以使用concurrent并发包下的concurrentHashMap。
java7 HashMap

HashMap的整体结构图:
在这里插入图片描述
图是盗来的,整体结构就是一个数组,数组里面元素是单向链表,链表结构实际是一个Entry,里面存放了key,value,hash以及next指针

public class HashMap<K,V>    
    extends AbstractMap<K,V>    
    implements Map<K,V>, Cloneable, Serializable    {    
    //初始容量为16,且实际容量必须是2的整次幂
     static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 初始容量
     //负载因子 loadFactor
     static final float DEFAULT_LOAD_FACTOR = 0.75f; 
    // HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子)
     int threshold;
     //负载因子的实际大小
     final float loadFactor;
    // 存储数据的Entry数组,长度是2的幂。    
    // HashMap采用链表法解决冲突,每一个Entry本质上是一个单向链表    
    transient Entry[] table; 
put 方法
public V put(K key, V value) {
       // 如果table是个空数组,进行数组填充,此时threshold值为初始容量值16,也就是插入第一个元素时进行初始化
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //如果当前的key可以为null,则将该键值对存入table[0]中
        if (key == null)
            return putForNullKey(value);
       //计算hash值
        int hash = hash(key);
        //找到对应的数组下标
        int i = indexFor(hash, table.length);
        //找到对应数组下标的链表,判断是否有重复的key值存在,如果有则覆盖,并返回旧值
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        //如果不存在重复的key值,就将其加入单链表中
        addEntry(hash, key, value, i);
        return null;
    }
indexfor
//根据key的hash值,计算出在数组中的位置
static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

这个方法简单说就是取hash值的低n位,如果length=32,也就是2的5次方,那么hash值进行位与运算时,结果就是hash值低5位为原值,其他高位与0进行与运算皆为0.

数组初始化 inflateTable()
  private void inflateTable(int toSize) {
    // 保证数组大小一定是 2 的 n 次方。
    // 比如这样初始化:new HashMap(20),那么处理成初始数组大小是 32
    int capacity = roundUpToPowerOf2(toSize);
    // 计算扩容阈值:capacity * loadFactor
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    // 初始化数组
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity); //ignore
添加节点到链表中
//添加链表到节点中
void addEntry(int hash, K key, V value, int bucketIndex) {
		//如果当前HashMap的大小已经达到阈值,并且要插入的位置已经有元素了,进行扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
        	//扩容为原来数组的两倍长
            resize(2 * table.length);
            //扩容以后重新计算hash值
            hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
            //重新计算扩容后的新下标
            bucketIndex = indexFor(hash, table.length);
        }
        //创建链表
        createEntry(hash, key, value, bucketIndex);
    }
    //将新数据插入到新扩容后的数组相应位置的链表的表头
    void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMapEntry<K,V> e = table[bucketIndex];
        //把原先的节点e作为插入的节点的next
        table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
        size++;
    }
static class HashMapEntry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        HashMapEntry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        HashMapEntry(int h, K k, V v, HashMapEntry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
  }
扩容
 void resize(int newCapacity) {
        HashMapEntry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        //新数组
        HashMapEntry[] newTable = new HashMapEntry[newCapacity];
        //将原来数组的元素全部移动到新数组中
        transfer(newTable);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
put()过程总结

向HashMap中插入一个元素,table是一个数组,数组中元素类型为Entry

  • 如果当前的HashMap为空,则初始化table
  • 如果插入的元素key为null,则将其插入到table[0]中。
  • 如果key不为null,并且HashMap也不为空
    • 计算出key的hash值
    • 根据计算的hash值,得出key在数组中的下标位置
    • 根据下标位置找到对应的链表,在链表中判断是否有重复的key值出现,如果有则覆盖并返回旧值,如果没有则将其插入到链表中。
  • 在添加节点到链表中时,如果当前的HashMap大小已经达到阈值,并且要插入的位置已经有元素了,就必须进行扩容
    • 扩容大小为原来数组长度的2倍,将旧数组中的元素全部迁移到新数组中
    • 由于是双倍扩容,那么原来table[i]链表中的节点,将分配到table[i]和table[i + oldLength]位置上,table[0]上的链表也就分配到了table[0]和table[16]这两个位置上去了
  • 扩容之后,需要重新计算hash值和要插入的新下标
  • 将新数据插入到新扩容后的数组相应位置的链表的表头
get过程分析
public V get(Object key) {
    // 之前说过,key 为 null 的话,会被放到 table[0],所以只要遍历下 table[0] 处的链表就可以了
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);
    return null == entry ? null : entry.getValue();
}

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        //确定数组下标,从头遍历此数组下标的链表中的元素,找到就返回,否则返回null
        for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }
  • 计算key值对应的hash值
  • 根据hash值,找到对应的数组下标
  • 从头遍历该数组位置上的链表,直到找到相等的key
java 8 HashMap

整体数据结构:
添加了红黑树,其中链表节点使用node表示,红黑树使用TreeNode表示,属性还是key,value,hash 和 next 这四个属性。

put过程
  //当链表长度大于8,就采用红黑树
  static final int TREEIFY_THRESHOLD = 8;

  public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
//第三个参数onlyIfAbsent,if true, don't change existing value,是指当key重复时,不进行put操作,也就是说当key不存在时,才进行put操作
  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //初始化tab长度
        if ((tab = table) == null || (n = tab.length) == 0)
        //对tab的初始化操作,扩容方法下面有具体分析
            n = (tab = resize()).length;
        //(n - 1) & hash,找到数组对应的下标
        //如果这个数组上没有值,就直接初始化node放置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {//如果这个数组上是有值的
            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);
                        //如果新插入的节点是第8个,则将链表转换成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);//转换红黑树方法
                        break;
                    }
                    //如果在链表中,找到对应的key值,则返回e,此时e为链表中与要插入的节点相等的node。
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果找到相同key,则进行值覆盖,返回旧值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //如果HashMap由于新插入的元素,超过了阈值,则需要进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
扩容resize()
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //数组扩容
        if (oldCap > 0) { 
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果数组扩容为原来的2倍小于最大值,并且原来的数组长度小于初始值,将数组扩容为原来的2倍,
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                 //将阈值也扩大为原来的2倍
                newThr = oldThr << 1; // double threshold
        }
        // initial capacity was placed in threshold
        else if (oldThr > 0) // 对应使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的时候
            newCap = oldThr;
        else { //对应使用new HashMap()进行初始化
            //大小设置为初始值16
            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的时候,直接返回newTab即可
        table = newTab;
        //原来数组大小不够,需要进行扩容,进行数据迁移
        if (oldTab != null) {
           //遍历数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //数组上是单个元素,直接将元素重新计算位置然后进行迁移就OK了
                    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 { //这里是对链表的处理
                        //将链表拆分成2个链表
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                        	//next是e节点的后继节点
                            next = e.next;
                            //根据当前节点的hash值与旧数组与运算是否为0来进行拆分链表,直到链表的尾节点
                            if ((e.hash & oldCap) == 0) {
                            	//空链表,就将e作为头节点
                                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) {//第二条链表放置到原来位置+原来长度的位置上,因为扩容之后的长度是原来的2倍
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
put过程总结

向HashMap中插入一个元素

  • 如果当前tab为null,则进行初始化,初始化数组大小为16,阈值设置为负载因子 * 初始大小,这是调用无参的构造函数时,调用有参的构造函数时,根据设置的容量,返回一个2的整次幂
  • 找到要插入key对应的数组下标,然后根据数组下标去判断数组是否有值
  • 如果对应数组下标没有值,则直接初始化node放置到数组中
  • 如果数组下标对应有值,先判断是否跟第一个数据相等,如果相等直接返回,如果不相等,接着判断是否是红黑树
  • 如果是红黑树,则调用红黑树的插入值方法putTreeVal
  • 如果不是红黑树,那就是链表,首先遍历链表,在链表中查找是否有相等的key,如果有,返回当前链表中与key相等的node,进行值覆盖,并返回旧值。如果没有则在插入在链表的尾节点之后(Java7是插入到表头),插入之后要去判断当前链表的长度是都已经超过设置的链表阈值8,如果超过则调用treeifyBin(tab, hash)方法转换为红黑树。
  • 如果已经插入成功,判断是否超过阈值,如果超过,则需要进行扩容
get过程分析
  1. 根据hash值找到对应的数组下标 (n - 1) & hash
  2. 判断数组当前位置元素是否是要找的,是就返回,不是就去判断下一个结点
  3. 判断下一个结点是否是红黑树,如果是,则调用红黑树的getTreeNode方法查找结点,如果不是,则4步
  4. 遍历链表,直到找到相等的key
  5. 如果都没找到,则返回null。
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;
        //根据hash值找到对应的数组下标(n - 1) & hash
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //判断与数组第一个数据是否相等,相等则返回first
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
             //与第一个元素不相等,则继续判断下一个
            if ((e = first.next) != null) {
               //判断是否是红黑树,如果是,则调用红黑树查找结点方法
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {//是链表,则遍历链表,找到与key相等的node,并返回
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
总结

HashMap仅仅适合于单线程环境,不适合并发操作,这里我只是单纯的对于源码的分析,像红黑树一类的,这里就没有展开分析,后面会更新一下,以前总觉得源码很吓人,多难多难,其实只要耐着性子,慢慢研究,还是能看的懂的,多看,多想,总是会进步的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值