Java源码分析3-HashMap

本文基于JDK1.8,详细分析了HashMap的构造方法、核心操作如扩容和键值对放入散列桶的机制,强调了初始化容量选择的重要性以避免频繁扩容,以及HashMap允许空键值的特点。
摘要由CSDN通过智能技术生成

以下内容基于JDK1.8
以下内容将分为如下几个章节

  • 概述
  • 继承关系图
  • 相关成员变量
  • 内部静态类
  • 构造函数
  • 常用方法
  • 优缺点

概述

  • 实现了所有Map的相关操作
  • 允许key为空或value为空
  • 除了允许空值和非同步外,其他与HashTable类似

继承关系图

相关静态成员变量

/**
 * 默认存储容量--16=2^4
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * 最大存储容量--1073741824=2^30
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 默认负载因子,即默认情况下当键值对数量大于时DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR=12时,会发生扩容
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 散列桶长度大于该值时,有可能会转化成红黑树
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 散列桶长度小于该值时,则会由树重新退化为散列桶
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 在转变成红黑树树之前,还会有一次判断,只有键值对数量大于该值时才会发生转换
 */
static final int MIN_TREEIFY_CAPACITY = 64;

相关成员变量

/**
  * 节点表,首次使用时初始化,并根据需要调整大小
  * 分配时,长度总是2的幂
  */
 transient Node<K,V>[] table;

 /**
  * 保留缓存entrySet(),用于抽象类的keySet()和values()方法
  */
 transient Set<Map.Entry<K,V>> entrySet;

 /**
  * 键值对个数
  */
 transient int size;

 /**
  * 修改次数
  */
 transient int modCount;

 /**
  * 下次扩容是size的阈值=容量*负载因子
  * The next size value at which to resize (capacity * load factor).
  */
 int threshold;

 /**
  * 负载因子
  */
 final float loadFactor;

内部静态类Node<K,V>

 static class Node<K,V> implements Map.Entry<K,V> {
 	//存储key对应hash值
    final int hash;
    //key
    final K key;
    //value
    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;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

构造方法

  1. 无参构造函数
public HashMap() {
	//设置负载因子为默认值
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
  1. 传入参数为容量构造函数
public HashMap(int initialCapacity) {
	//源码见3
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
  1. 传入参数为容量和负载因子的构造函数
public HashMap(int initialCapacity, float loadFactor) {
	//判断传入容量是否小于0
    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);
}
  • 3.2 返回大于且最接近cap的2的n次幂 tableSizeFor方法
  • 例如,当cap=3时,则返回4
  • 当cap=12时,则返回16
  • 当cap=16时,则返回16

即返回值>=传入值

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
  1. 传入参数为Map实现类构造函数
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}
  • 4.1 putMapEntries(Map<? extends K, ? extends V> m, boolean evict)方法
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
	//获取元素个数
    int s = m.size();
    
    if (s > 0) {
        if (table == null) { // pre-size
        	//计算其容量=元素个数/负载因子+1
            float ft = ((float)s / loadFactor) + 1.0F;
            //判断其容量是否大于最大容量
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            //判断容量是否大于0
            if (t > threshold){
            	//计算其下次扩容阈值
            	threshold = tableSizeFor(t);
            }
                
        }else if (s > threshold){
        	//扩容方法
    	   resize();
        }
        //迭代map子类
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
        	//获取key
            K key = e.getKey();
            //获取value
            V value = e.getValue();
            //将键值对放入到散列桶中,hash(key)为计算key的hash值
            putVal(hash(key), key, value, false, evict);
        }
    }
}

其中,resize方法和putVal是HashMap比较核心的方法
由于putVal调用了resize方法,先来看resize方法吧

  • 4.2 扩容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;
    //判断散列桶长度是否大于0
    if (oldCap > 0) {
    	//判断散列桶长度是否超过最大容量
        if (oldCap >= MAXIMUM_CAPACITY) {
        	//扩容阈值=整型最大值
            threshold = Integer.MAX_VALUE;
            //已到最大容量,不能再扩容了,直接返回原散列桶
            return oldTab;
        }else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY){
            //设置新容量=散列桶长度*2
            //若新容量小于最大容量且散列桶长度>=16
            //新扩容阈值=扩容阈值*2
        	newThr = oldThr << 1; // double threshold
       	}
    }else if (oldThr > 0){
    	//若散列桶长度=0且扩容阈值>0
		
		//新容量=扩容阈值
    	newCap = oldThr;
    }else {
 	     //若散列桶长度=0且扩容阈值=0
 	    
 	    //新容量=默认初试容量,即16         
        newCap = DEFAULT_INITIAL_CAPACITY;
        //新扩容阈值=默认初试容量*默认负载因子,及16*0.75-12
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //判断新扩容阈值是否为0
    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;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //迭代散列桶判断对应节点是否为空
            if ((e = oldTab[j]) != null) {
            	//将对应元素置为空
                oldTab[j] = null;
                //判断节点是否有下一个节点
                if (e.next == null){
                	//无下一个节点,则重新计算节点在新散列桶中的索引
                	//新索引值=节点hash值&新散列桶长度-1
                	//这么做的好处是啥,此处有一个前提,那就是散列桶的长度是2的n次幂
                	
                	//假设新散列桶长度为16,那么newCap - 1=15
                	//例子1 e.hash=03,索引=03&15=	0000 0011 
              		//                         	  & 0000 1111 
              		//							  = 0000 0011 = 03

					//例子2 e.hash=07,索引=07&15=	0000 0111 
              		//                         	  & 0000 1111 
              		//							  = 0000 0111 = 07
              		
                	//例子3 e.hash=13,索引=13&15=	0000 1101 
                	//                   		  & 0000 1111 
                	//							   =0000 1101 = 13
                	
            	    //例子4 e.hash=16,索引=16&15=	0001 0000 
            	    //							  & 0000 1111 
            	    //							  = 0000 1101 = 00
            	    
        	        //例子5 e.hash=25,索引=25&15=	0001 1001 
        	        //							  & 0000 1111 
        	        //							  = 0000 1001 = 09
						
					//假设新散列桶长度为12,那么newCap - 1 = 11,已同样的例子来对比就是另外一种情形
					//例子1 e.hash=03,索引=03&11=	0000 0011 
					//							  & 0000 1011 
					//							  = 0000 0011 = 03
					
					//例子2 e.hash=07,索引=07&11=	0000 0111 
              		//                         	  & 0000 1011 
              		//							  = 0000 0011 = 03
					
                	//例子2 e.hash=13,索引=13&11=	0000 1101 
                	//							  & 0000 1011 
                	//							  = 0000 1001 = 09
                	
            	    //例子3 e.hash=16,索引=16&15=	0001 0000 
            	    //							  & 0000 1011 
            	    //							  = 0000 1101 = 01
            	    
        	        //例子4 e.hash=25,索引=25&15=	0001 1001 
        	        //							  & 0000 1011 
        	        //							  = 0000 1001 = 09

        	        //对比发现,已通过e.hash & (newCap - 1)分配散列桶索引
        	        //只要保证节点的hash值尽可能的不重复,那么节点就可以越均匀分布在基于数组散列桶中,从而保证效率

     	        	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) 例1
						//e.hash=09 0000 1001
						//oldCap=16	0001 0000
						//	二者  &=	0000 0000=0						

						//(e.hash & oldCap) 例2
						//e.hash=13 0000 1101
						//oldCap=16	0001 0000
						//	二者  &=	0000 0000=0

						
						//(e.hash & oldCap) 例3
						//e.hash=31 0001 1111
						//oldCap=16	0001 0000
						//	二者  &=	0001 0000=16

						//(e.hash & oldCap) 例4
						//e.hash=25 0001 0011
						//oldCap=16	0001 0000
						//	二者  &=	0001 0000=16
						
						//从上述例子可得结论和代码:
						//当节点的hashcode>=oldCap时,e.hash & oldCap > 0
						//当节点的hashcode<oldCap时,e.hash & oldCap = 0

						//loHead,loTail的含义是低位的头结点和尾结点
						//hiHead,hiTail的含义是高位的头结点和尾结点
							
						//那何为高位呢,就是如果节点对应的hash值大于等于散列桶长度
						//低位则相反,其hash值小于散列桶长度
						
						//以下仅几行代码就代码实现了如下功能
						//1.重新计算节点在新散列桶中的位置
						//2.保证节点在散列桶中的顺序与在扩容前一致
						
                        if ((e.hash & oldCap) == 0) {
                        	
                            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);
                    //当loTail不为空,即节点的hashcode<oldCap时,节点在新散列桶中的位置不变
                    if (loTail != null) {
                    	//尾结点已经是最后一个节点了,其下一节点需置为空
                        loTail.next = null;
                        //将低位头结点放入新散列桶索引值=[j]处
                        //低位节点的位置在扩容后相对于老散列桶来说位置未发生变化
                        newTab[j] = loHead;
                    }
                    //当hiTail不为空,即节点的hashcode>=oldCap时,节点在新散列桶中位置=原位置+原散列桶长度
                    if (hiTail != null) {
                        hiTail.next = null;
                        //将高位头结点放入新散列桶索引值=[j+老散列桶长度]处
                        //高位节点的位置在扩容后相对于老散列桶来说位置向后挪动了[老散列桶长度]个位置
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

分析完resize()方法,可以得出如下结论:

  • 设置扩容阈值的tableSizeFor(初始容量)方法能够保证散列桶的长度为2的n次幂,也就是说
    元素能均匀分布在散列桶中,保证在进行增,删,改,查时效率最高

  • 如果已知放入元素个数时, 最好是通过2或3构造函数得到HashMap实例,
    再进行操作,否则在且添加元素较多时,就需要频繁扩容,那就得不偿失了

  • 如果未知放入元素个数时,就需要进行提前预判了,既不要太大浪费空间,也不要太小避免频繁扩容


  • 4.3 将键值对放入散列桶putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)方法
 /**
 * 实现了Map接口的put和related方法
 * @param hash--key对应的hash值
 * @param key--放入的key
 * @param value--放入的key对应的value值
 * @param onlyIfAbsent--若为true,则不覆盖已存在的值
 * @param evict--若为false,散列桶将处于创建模式,此参数在HashMap中无意义
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    //当前散列桶             
    Node<K,V>[] tab; 
    //p为散列桶中索引为i的头节点或迭代时的节点
    Node<K,V> p;
    //n为散列桶长度, i为键值对在散列桶中的索引值
    int n, i;
    //判断散列桶是否为空
    if ((tab = table) == null || (n = tab.length) == 0){
    	//若为空,则调用扩容方法进行扩容
    	n = (tab = resize()).length;
    }
    
    //判断在散列桶中索引为i处是否为空    
    if ((p = tab[i = (n - 1) & hash]) == null){
    	//头结点p为空则初始化一个节点后将其放入
    	tab[i] = newNode(hash, key, value, null);
    }else {
    	//头结点p不为空
    	//e用来存储p结点的下一节点
        Node<K,V> e;
        //已存在节点的键
        K k;
        //判断
        //1.节点p的hash值是否与传入键的hash值一致
        //2.(节点p的key是否与与传入key的内存地址相同)或(传入key不为空且传入键与已存在键相同)
        if (p.hash == hash &&
          	((k = p.key) == key 
          	|| (key != null && key.equals(k)))){
            //若1,2均满足则用e先存储p
           	e = p;
         }else if (p instanceof TreeNode){
        	//红黑树忽略
        	e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        }else {
        	//hash值和键值既不相同,且不为红黑树
        	//迭代中索引为i的节点,binCount用来记录散列桶中索引为i处的元素个数
            for (int binCount = 0; ; ++binCount) {
            	//判断p下一节点是否为空
                if ((e = p.next) == null) {
                	//为空则说明p下面无节点,直接创建新节点并将其设置为p的下一个节点
                    p.next = newNode(hash, key, value, null);
                    //如果元素个数大于等于8,则转为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) {// -1 for 1st
                    	treeifyBin(tab, hash);
                    }
                    break;
                }
                //判断
        		//1.节点e的hash值是否与传入键的hash值一致
        		//2.(节点e的key是否与与传入key的内存地址相同)或(传入key不为空且传入键与已存在键相同)
                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){
            	//若onlyIfAbsent为false或节点存储值不为空,则用传入值替换旧值
            	e.value = value;
            }
            //在HashMap中该方法只做了声明,未实现任何逻辑     
            afterNodeAccess(e);
            //返回旧值
            return oldValue;
        }
    }
    ++modCount;
    //判断放入键值对后,是否需要进行扩容
    if (++size > threshold){
    	//大于扩容阈值,则需进行扩容
    	resize();
    }  
    //在HashMap中该方法只做了声明,未实现任何逻辑
    afterNodeInsertion(evict);
    return null;
}

常用方法

1 放入键值对put(K key, V value)方法

/**
 * @param key 放入键
 * @param value 放入值 
 * @return 返回key之前存储值,若HashMap中之前未存储key,那么返回空
 */
public V put(K key, V value) {
	//详见4.3
    return putVal(hash(key), key, value, false, true);
}
  1. 通过键删除元素remove(Object key)方法
 /**
 * @param  key 删除键
 * @return 返回key之前存储值,若HashMap中之前未存储key,那么返回空
 */
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}
  • 2.1 通过key对应的hash值,key值删除元素removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)方法
/*
 * @param hash--key对应的hash值
 * @param key--删除的key
 * @param value--删除的key对应的值
 * @param matchValue--若为true,则仅删除匹配的值
 * @param movable--若为false,则在删除时不移动其他节点
 * @return 之前存储节点,若无key对应的节点将返回空
 */
final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
    
    //用于存储散列桶                           
    Node<K,V>[] tab; 
    //p为散列桶中索引为i的头节点或迭代时的节点
    Node<K,V> p;
    //n为散列桶长度,
    //index为(n - 1) & hash
    int n, index;
    //判断散列桶是否为空且长度大于0
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        //node用于存储匹配的的节点
        //e节点用于存储p节点的下一节点
        Node<K,V> node = null, e; K k; V v;
        //判断
    	//1.头节点的hash值是否与传入键的hash值一致
    	//2.(头节点的key是否与与传入key的内存地址相同)或(传入key不为空且传入键与已存在键相同)
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k)))){
            node = p;
        }else if ((e = p.next) != null) {
        //头结点的下一节点不为空
        
        	//判断是否为红黑树
            if (p instanceof TreeNode){
            	node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            }else {
            	//迭代e下一节点
                do {
                	//判断
    				//1.e节点的hash值是否与传入键的hash值一致
    				//2.(e节点的key是否与与传入key的内存地址相同)或(传入key不为空且传入键与已存在键相同)
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        //判断
        //1.是否匹配到key对应的节点node
        //2.是否删除不匹配的值或node对应的值与传入值相同
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
			//判断是否为红黑树                             
            if (node instanceof TreeNode){
            	((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            }else if (node == p){
            	//当node == p仅发生在p为头结点时
            	//即直接将index对应的头结点置为node的下一节点
            	tab[index] = node.next;
            }else{
            	//p此时为node的上一节点,要删除node,直接将p的下一节点跳过node而指向node的下一节点
            	p.next = node.next;
            }
            ++modCount;
            --size;
            //在HashMap中该方法只做了声明,未实现任何逻辑
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值