HashMap实现原理分析(2)

本来介绍一下HashMap最重要的两个方法,get和put。在阅读文本之前,请先阅读HashMap实现原理分析(1) 。

  • HashMap中大致流程

下面先看一下这些HashMap在实现过程中的一些基本属性。

//数据实际存储结果
transient Node<K,V>[] table;
// table中时间存储的元素个数
transient int size;
//下一次需要对table进行扩容的临界值
int threshold;
// 扩容时的平衡因子 final float loadFactor;

下面是默认常量

//默认table的初始化大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
// table 最大长度
static final int MAXIMUM_CAPACITY = 1 << 30; 
// 默认的加载因子,用于对table进行扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当链表转红黑树时,链表最大长度
static final int TREEIFY_THRESHOLD = 8;
// 当树的元素小于此值时,会将红黑树结构转换成链表结构
static final int UNTREEIFY_THRESHOLD = 6;
// 当链表转红黑树时,table最小长度
static final int MIN_TREEIFY_CAPACITY = 64

通常使用不带参数的new HashMap()方式创建一个HashMap对象,该对象就会默认使用DEFAULT_LOAD_FACTOR作为平衡因子。在用户第一次执行put操作的时候,会把table初始化为一个大小为DEFAULT_INITIAL_CAPACITY的数组。

每size>threshold的时候,就会对table进行扩容,threshold的值=loadFactor*table.length。因此当loadFactor越大,扩容触发就越晚,越节省内存空间,但是发生hash冲突越容易。相反当loadFactor越小,扩容触发就容易,就会需要消耗更多的内存空间,但是发生hash冲突相对比较难。因此默认的0.75的平衡因子是容量和性能之间平衡。

当table的同一个位置已经存在元素的时候,这时候相同位置的元素会变成一个链表。如果链表的长度已经达到默认值TREEIFY_THRESHOLD并且table中的元素个数达到MIN_TREEIFY_CAPACITY,链表结构会转换成红黑树。当HashMap中的元素被删除之后,如果树的元素个数小于UNTREEIFY_THRESHOLD,红黑树又会转换为链表结果。

用户执行put操作后,把key和value的封装成为Node对象,然后放入到table中。

static class Node<K,V> implements Map.Entry<K,V> {        
  // put操作key的hash值        
  final int hash;        
  // put操作的key        
  final K key;        
  // put操作的value       
  V value;        
  // 指向下一个hash相同的Node,用来简历链表和树        
  Node<K,V> next;   
}
  • get方法

通过get(key)方法获取key所对应的value值。具体如下:

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 &&          
             ((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 {                   
                   //hash和key必须同时满足相等
                   if (e.hash == hash &&                       
                         ((k = e.key) == key || (key != null && key.equals(k))))                       
                   return e;               
              } while ((e = e.next) != null);
        }        
  }      
  return null;
}

通过上面源码可知,get方法会调用getNode方法,在调用getNode方法之前,把key调用hash方法计算获取key的hash值,hash方法的实现,请参考在HashMap实现原理分析(1) 。getNode方法第一步是把多个步操作融合在一起了:

if( (tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null)

 

这条if语句注意完成两件事:

第一:把table赋值给tab。table是HashMap实际存放数据的数组,以后在getNode方法中操作tab就相当于操作table数组;

第二:判断HashMap是否为空。当该HashMap创建后,没有进行过put操作,那么tab == null。

第三:首先把判断tab[(n - 1) & hash])位置的元素复制给first,后面可以直接通过操作first就比较方便。然后判断fisrt的值是否为null。关于(n - 1) & hash这含义也请参考HashMap实现原理分析(1)。

如果if条件成立的话,那说明就是传入的key是在此HashMap中是存在的。

下面来进入第二个if语句:

 if (first.hash == hash &&
        ((k = first.key) == key || (key != null && key.equals(k))))

这里的是先判断fisrt的hash是否与key的hash相同,如果hash相同,然后比较first.key与key是否相等。只有当hash与key值同时满足相等,first才是所有查找的对象,最后返回first。

如果上述if条件不满足,并且 fisrt.next的是否为空,则对fisrt.next后续节点进行遍历查找:这里又会分为两种情况,如果fisrt是已经是红黑树,那么遍历红黑树,否则把fisrt作为链表的头结点,从链表中查找同时满足hash和key值相等的Node对象,并返回。

  • 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; 
        //n是table的大小 i是table的索引
        int n, i;
        //1.把table赋值给tab,方法内部操作tab来代替操作table
        //2.判断tab是否为null
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //3.如果对应tab[i]的位置为null,把新创建的Node赋值到tab[i]
        //  这里还有一个赋值操作p=tab[i],这样后面操作tab[i],可以用操作p来代替
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //当HashMap中已经存在了参数key,那么e指向旧的Node对象或者是null
            Node<K,V> e; 
            K k;
            // 4. p.hash和key hash 以及p.key和key如果同时相等,那么用新value代替旧的value
            //    这里没有立即执行e.value=value,而放在了最后。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 5.当发现p.key与key不相等,且p是红黑树
            else if (p instanceof TreeNode)
                //6. 把新key和value插入到红黑树中,并且返回旧的Node对象(如果存在key相同的旧Node对象)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //7.如果p是个链表,那么新key和value创建的Node节点,假如到p链表的尾部      
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //8.当链表的大小超过了链表与红黑树转换的临界值的时候,把链表转为树
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 9. 当链表中找到一个与方法参数key的hash和key同时相等的节点后,停止链表遍历
                    //    此时e就指向了链表中与参数key一致的Node节点
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //10. 如果e不为null表示e是老值,那么put方法返回HashMap之前存在的就value
            if (e != null) { 
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //11. 当HashMap中的元素个数大于进行扩容的临界值,那么对table进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

在上面的源码中已经给出了比较详细注释信息,通过这些注释,可以大致了解HashMap在执行put操作的流程,或者参考下面的流程图。这里还有一些具体的执行细节,比如红黑树的遍历,链表与红黑树的转换等由于篇幅有限,没有做过多介绍。

HashMap实现原理分析(2)
上一篇:深入react技术栈(7):组件化实例:Tab栏组件


下一篇:前端editorconfig使用详解