集合源码分析之 HashMap

Posted Jerry

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了集合源码分析之 HashMap相关的知识,希望对你有一定的参考价值。

一 知识准备

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

二  HashMap的数据结构:

  JDK 7.0及以前

     在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

   从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

 

  JDK 8.0(本文主要介绍JDK 8.0的实现)

JDK7.0及以前,HashMap的结构都是基于一个数组以及多个链表的实现,处理Hash冲突的方法就是将对应节点以链表的形式存储。

简单的实现是以HashMap性能牺牲为代价的,如果说有成百上千个节点在hash时发生碰撞,存储一个链表中,那么如果要查找其中一个节点,那将不可避免的花费0(N)的查找时间,严重影响性能。JDK 8.0开始使用数组+链表+红黑树的组合来实现HashMap

(http://www.cnblogs.com/leesf456/p/5242233.html


三 字段

//HashMap的散列表
transient Node<K,V>[] table;

//存放entry的set
transient Set<Map.Entry<K,V>> entrySet;

//记录HashMap中存储了多少个键值对<KEY-VALUE>
transient int size;

//mod是modify的缩写,hashMap的结构发生结构变化时会记录一次。
transient int modCount;

//默认初始化table的大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//table的最大大小
static final int MAXIMUM_CAPACITY = 1 << 30;
//这是一个比例参数,当table中已经被占用的元素数与table总长度的比例不小于这个参//数的时候,就会发生table的扩容,每次扩容都以2倍大小进行扩容,注意resize()函数
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//当size大于这个数时,就进行一次扩容,即调用resize()函数
int threshold;

//当节点冲突数达到8时,就会对hash表进行调整,如果table的长度小于64,那么会进//行table扩容,如果不小于64,那么会将因冲突形成的单链表调整为红黑树。
static final int TREEIFY_THRESHOLD = 8;

//在删除冲突节点之后,同hash的节点数低于这个值时,将红黑树重新恢复为单链表。
 static final int UNTREEIFY_THRESHOLD = 6;

//注意到TREEIFY_THRESHOLD解释,不小于64时仅对table进行扩容,这个64就是//指这个值。
static final int MIN_TREEIFY_CAPACITY = 64;

四 构造函数

 

  /**
     * @param  initialCapacity 初始容量
     * @param  loadFactor      负载因子*/
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
        
     this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } /** * @param initialCapacity 初始容量,默认负载因子0.75*/ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * 默认初始容量16,默认负载因子 0.75 */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; } /** * @param 使用一个map来初始化新的HashMap*/ public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }

 

五 Get 和 Put 方法

put方法

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

putVal方法

 /**
     * Implements Map.put and related methods
     * @param hash hash for key
     * @param key 键
     * @param value 值
     * @param onlyIfAbsent 如果是true,不改变已存在的值,字面意思,只当map中该对象没有才存入,默认false
     * @param evict 如果 false, 散列表处于创建模式,默认true
     * @return 返回旧值或者null或者 none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
      //先判断,table大小,如果table为null,或者没分配空间,就resize一次
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //如果首节点为null,就创建一个首节点。注意到tab[i = (n - 1) & hash],(n-1)&hash才是真正的hash值,也就是存储在table的位置(index)。
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);//创建一个新的节点
        else {//冲突处理
            Node<K,V> e; K k;
        //p这时候是指向table[i]的那个Node,这时候先判断下table[i]这个节点是不是和我们待插入节点有相同的hash、key值。如果是就e = p
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
      //这里说明第一个节点的hash、key值与我们带插入Node的hash、key值不吻合,那么要从这个节点之后的链表节点或者树节点中查找。由于之前提到过,1.8的HashMap存储碰撞节点时
      ,有可能是用红黑树存储,那么先判断首节点p的类型,如果是TreeNode类型(Node的子类),那么就说明碰撞节点已经用红黑树存储,那么使用树的插入方法,如果新插入了树节点,
      那么e会等于null,用于后面的判断与处理
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) {//e引用下一个节点,如果是null,表示没有找到同hash、key的节点 p.next = newNode(hash, key, value, null);//创建一个新的节点,放到冲突链表的最后               // 注意到如果这时候冲突节点个数达到8个,那么就会treeifyBin(tab, hash)函数,看是否需要改变冲突节点的存储结构,
               这个treeifyBin首先回去判断当前hash表的长度,如果不足64的话,实际上就只进行resize,扩容table,如果已经达到64,那么才会将冲突项存储结构改为红黑树。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; }             //如果找到了同hash、key的节点,那么直接退出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e;//调整下p节点 } } if (e != null) { // existing mapping for key          //注意到这时候要判断是不是要修改已插入节点的value值,两个条件任意满足即修改 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e);//这个是空函数,可以由用户根据需要覆盖 return oldValue; } } ++modCount;//当插入了新节点,才会运行到这儿,由于插入了新节点,整个HashMap的结构调整次数+1 if (++size > threshold)//HashMap中节点数+1,如果大于threshold,那么要进行一次扩容 resize(); afterNodeInsertion(evict);//这个是空函数,可以由用户根据需要覆盖 return null; }

get方法,比较简单,就是在在table上根据key.hash查找,如果hash值相同有多个,则根据key.equals()在链表或者红黑树上遍历比较,得到最终值

 public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
/**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    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;
    }

六 总结

JDK 8.0HashMap中没当冲突节点个数大于8时,就先尝试table扩容,当table数达到64后,冲突节点数为8时,则进行链表向树结构转换,这样对于冲突节点的访问复杂度就会大幅度降低,当然这是建立在插入时冲突处理算法复杂度提升为代价的。

以上是关于集合源码分析之 HashMap的主要内容,如果未能解决你的问题,请参考以下文章

java源码分析之集合框架HashMap 10

集合源码分析之 HashMap

java集合之HashMap源码分析

集合框架JDK1.8源码分析之HashMap 转载

死磕 java集合之LinkedHashMap源码分析

Java集合之LinkedHashMap源码分析