• HashMap


    HashMap是Java Collection Framework重要成员,也是基于哈希表使用最多的Collection,以key-value形式存储数据,但由于其线程不安全性不用于多线程编程,但用途依然广泛,在JDK1.8上,HashMap的源码又加入了新的内容,可见其重要。这次就是基于JDK1.8的源码来理解HashMap的运行原理。

    继承

    public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    

    这里可以看到HashMap是继承AbstractMap抽象类并且依赖Map接口的,而其中AbstractMap则是对Map接口进行了部分实现,主要基于迭代器。下面就是在AbstractMap中对get方法的实现。

    public V get(Object key) {
        Iterator<Entry<K,V>> i = entrySet().iterator();
        if (key==null) {
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                if (e.getKey()==null)
                    return e.getValue();
            }
        } else {
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                if (key.equals(e.getKey()))
                    return e.getValue();
            }
        }
        return null;
    }
    

    构造和初始化

    在了解HashMap的构造函数之前首先需要知道HashMap的数据结构,也就是如何去存储。HashMap是基于哈希表的,因此底层肯定有一个作为哈希表的数组。而了解数据结构哈希表的都知道,哈希表在存储过程中存在冲突的问题,即存在多个key的哈希值相同,HashMap的做法是链表或者红黑树的方法,将相同哈希值的元素以链的形式连接在哈希表对应的位置。
    哈希表
    这里需要注意的是关于红黑树的部分,在JDK1.7的部分是只有链表结构,但是由于链表过长的时候查找元素时间较长,在JDK1.8的时候加入了红黑树,当链表超过一定长度之后,链表就会转换成红黑树,便于查找和插入。
    HashMap的构造函数有三种

    • HashMap() 默认初始容量(16)和默认加载因子(0.75)的空HashMap
    • HashMap(int initialCapacity) 指定初始容量和默认负载因子(0.75)的空HashMap
    • HashMap(int initialCapacity, float loadFactor) 指定初始容量和负载因子的空 HashMap

    这里需要了解两个参数,initialCapacity(初始容量),指最开始创建哈希表数组的大小。loadFactor(负载因子),指哈希表扩容的阈值,一般是当前表中存有元素与哈希表容量相除的商。

    put和get

    从JDK1.8开始,HashMap的元素是以Node形式存在,主要结构如下

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        //类方法,这里省略
        .........
    }
    

    包括元素的hash值,key,value以及指向下一个元素的引用。

    put

    put方法是HashMap的重点,包含了阈值校验,链表到红黑树的调整以及初始开辟哈希表,都是在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;
    //哈希表未创建或者长度为0时创建哈希表
    //resize()是重建哈希表的方法
       if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
     //如果存入元素位置为空,创建一个node类,插入
       if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
    //存入元素位置有元素
        else {
            Node<K,V> e; K k;
        //存在相同的key,替换原先的值
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
        //存储的链表已经变为红黑树,调用putTreeVal()方法去存入
            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) {
                    //没有相同key元素,就最后加上该元素
                        p.next = newNode(hash, key, value, null);
    		//链表过长的时候,转变成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //找到相同key元素,替换value
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null) //true || --
                    e.value = value;
    	   //3.
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
    //判断阈值,决定是否扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    

    这里有简单的流程图表示这个过程
    put
    对于resize()的描述就不深入了,这里简单讲一下HashMap的扩容原则

    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
    

    这里可以看到,每次哈希表的扩容是在上一次的容量基础上乘以2。

    get

    get方法相对于put就简单很多,就是根据哈希值找到相应的元素链,然后遍历查找元素即可,源码如下

    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;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //判断首元素是否为所找的
            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 {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
    

    HashMap死循环

    在JDK1.7多线程情况下HashMap存在死循环的情况,主要是由于扩容的过程是非线程安全的,并且在元素存入新表的过程中会出现元素倒置,由于插入过程,新插入元素总是插入到头部,源码如下

    void transfer(Entry[] newTable)  {  
    	Entry[] src = table;  
    	int newCapacity = newTable.length;   
    	for (int j = 0; j < src.length; j++) {  
        	Entry<K,V> e = src[j];  
        	if (e != null) {  
            	src[j] = null; 
            	//这里在做插入的过程中,总是将新插入元素放在链表头 
            	do {  
                	Entry<K,V> next = e.next;  
                	int i = indexFor(e.hash, newCapacity);  
                	e.next = newTable[i];  
                	newTable[i] = e;  
                	e = next;  
            	} while (e != null);  
        	}  
    	}  
    }
    

    具体的过程用图来表示,假设有两个线程,同时到了扩容的阶段,线程一在while循环开始就挂起了,线程二执行结束
    过程1
    之后线程一开始执行,从e节点开始,但是注意由于线程二扩容的影响,此时next节点的下一个节点是e
    过程2
    之后再往下遍历的时候,发现又回到了key=3,出现了环
    过程3
    这个问题在JDK1.8中解决了,它用了新的插入方式

    Node<K,V> loHead = null, loTail = null;
    Node<K,V> hiHead = null, hiTail = null;
    Node<K,V> next;
    do {
    	next = e.next;
    	//还是原索引位置
    	if ((e.hash & oldCap) == 0) {
        	if (loTail == null)
        		loHead = e;
    		else
    			loTail.next = e;
    		loTail = e;
    	}
    	//原索引+ oldCap位置
    	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;
    } 
    

    由于扩容是在原容量基础上乘以2,那么hash码校验的时候会比原来的多一位,那么只需要比较这个位置是0还是1即可,是1那么就在原索引位置向后位移原容量大小即可,是0就不动。
    扩容

  • 相关阅读:
    python入门之函数及其方法
    Python入门知识点2---字符串
    Python列表 元组 字典 以及函数
    Python入门知识
    Autofac使用代码
    优化EF以及登录验证
    CRM框架小知识以及增删查改逻辑代码
    分页SQL
    触发器SQL
    动态生成lambda表达式
  • 原文地址:https://www.cnblogs.com/xudilei/p/6843338.html
Copyright © 2020-2023  润新知