JAVASE集合(HashMap底层实现)下

浅显的api和一些底层概念

关于一些基础的API,我的上一篇笔记里面记载了,基础的API是比较容易上手的,但是容易的东西往往不是重点了,所以这里先不做过多记录。

HashMap的结构

和大家的理解是一样的,存放键值对的一个动态容器,key是存放"键"的,不能重复存放,重复存放相同的key类似于更新操作,value是"值",二者都是Object类型,而底层是啥样的呢,如图为jdk8的结构
在这里插入图片描述在此之前的jdk7是只有数组+链表完成底层数据结构的,即

jdk7:------数组+链表
jdk8:------数组+链表/红黑树(二叉树)

这个结构示意图和我们许多初学SE的同学来说是懵B的,因为先不说数组结构了,为什么Entry对象还能存放在链表形式存放,二叉树呢?
所谓HashMap的动态性由数组来体现,当数组的长度达到一定的长度,也就是put方法中存放的机制,会将数组扩容,而我们都知道HashMap存放对象是依照对象key的hashCode的,而不同的对象的HashCode是有可能相同的,这样又如何存放呢?Map做了一个处理就是,存放对象进来的时候,现将key对象的hashCode进行按位与和左移等运算进行重写,再对当前数组的长度进行按位与和左移操作返回一个数组的下标,于是乎,对象就能最大程度均匀分配到每个下标中,而总会存在下标一样的对象,这时,Entry对象进行引用连结,形成了链表,而链表的长度太长时可能造成效率问题,于是乎,在链表达到一定长度的时候,进行树化操作。

源码跟踪

基本参数

几个常量和变量:
(1) DEFAULT inItIAL_ CAPACITY: 默认的初始容量16
(2) MAXIMUM_ CAPACITY: 最大容量1 << 30
(3) DEFAULT LOAD_ FACTOR:默认加载因子0.75
(4) TREEIFY_ THRESHOLD:默认树化阈值8,当链表的长度达到这个值后,要考虑树化
(5) UNTREEIFY_ THRESHOLD:默认反树化阈值6,当树中的结点的个数达到这个阈值后,要考虑变为链表
(6) MIN_ TREEIFY CAPACITY:最小树化容量64
当单个的链表的结点个数达到8,并且table的长度达到64,才会树化。
当单个的链表的结点个数达到8,但是table的长度未达到64,会先扩容
(7) Node<K,V>[] table: 数组
(8) size:记录有效映射关系的对数,也是Entry对象的个数
(9) threshold:阈值,当size达到阈值时,考虑扩容
(10) loadFactor:加载因子,影响扩容的频率

初始化

new HashMap()

public HashMap() {
    
    
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    
}

此时的threshold为,table为null,size为0

put添加

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

这里点进去源码发现,还调用了putVal()和hash()方法,继续跟进

  • hash(Object key

目的:干扰hashCode的值,使得最终尽量散列存储
如果key值为null,则返回hash值----0
如果不为null,则返回 key的哈希值和 其 哈希值高16位(int型32位,右移16位则就是取高16位的值)进行异或
也就是将hashCode的高16位与低16位进行异或干扰运算,减少重复的概率

   static final int hash(Object key) {
    
    
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
  • final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
    在这里插入图片描述第一个参数:key的hash值
    第二个参数:key对象
    第三个参数:value对象
    第四个参数:若为true则保留原来的value,否则则替换
    第五个参数:若为false,则表处于创建模式。
    其中定义了许多参数,这里结合下面代码注释一下
Node<K,V>[] tab;  注释:建立临时数组后面赋值成HashMap的数组
 Node<K,V> p;     注释:新建一个结点,作为交换变量
 int n, i;        注释:n为tab的长度,i为tab的下标

继续往下看则有如下

扫描二维码关注公众号,回复: 11851732 查看本文章
if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

这里将临时的数组tab赋值成HashMap的数组,n赋值成数组的长度,判断如果HashMap是空的则调用 resize方法,我们继续跟进

  • resize()
 if (oldCap > 0) {
    
    
            if (oldCap >= MAXIMUM_CAPACITY) {
    
    
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {
    
                   // zero initial threshold signifies using defaults
            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;

这里截取片段。排除了不满足的条件剩下来的代码
作用就是,给HashMap的数组进行初始化参数,长度和阈值,并且将一个长度为16达到数组返回赋值给数组
然后返回puValue继续则有判断:

if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

意思就是,如果数组为空,则创建一个Node类型的key-value这一映射关系结点
继续看源码,如下

 if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

判断,加进来的结点和数组的第一个结点重复了,用e结点去保存tab的第一个结点

 else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

继续判断tab的第一个元素是否是树节点,则单独处理树节点
继续向下执行不是第一个元素,没有重复的判断

//table[i]的第一个结点不是树结点,也与新的映射关系的key不重复
for (int binCount = 0; ; ++binCount) {
    
    
	//如果p的下一个结点是空的,说明当前的p是最后一个结点
	if ((e = p.next) == null) {
    
    
		//把新的结点连接到table [i]的最后
		p.next = newNode (hash, key,value,nu1l) ;
		if (binCount >= TREEIFY TRESHOLD - 1) {
    
     // -1 for 1st
			treeifyBin(tab,hash);//树化操作
		}
	break;
	}
	//如果key重复了,就跳出for循环
	if (e.hash == hash &&((k = e.key) == key川(key != null && key.equals(k)))) 
	break;
	//继续往下循环判断
	P =e;
}

循环就是将结点p给保存到tab数组中,并且过程中还会判断是否需要树化链表。接着跳出循环进行下一个判断

	//如果这个e不是null,说明有key重复,就考虑替换原来的value
	if (e != null) {
    
     / / existing mapping for key
		V oldValue = e. value ;
	if (!onlyI fAbsent| | oldValue == null) {
    
    
		e. value = value ;
	}
	afterNodeAccess (e) ;
	return oldValue ;

这里从循环跳出,如果e不为空,就说明循环是break结束的,说明有key重复,就进行更新value的操作
紧接着

		 ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;

数组的元素个数加一,并且和阈值进行比较,如果达到了阈值,进行resize扩容数组(扩容的倍数是两倍),同时阈值也会进行更新,因为扩容是两倍,所以阈值也会增加两倍

  • 总结:

(1)如果第一次添加时,把table初始化为长度为16的数组,threshold = 12
(2)如果不是第一-次添加
①会考虑是否key有重复,那么就替换value
②如果table[i]下面不是树,统计table[i]的结点的个数,添加之前达到7个,考虑树化

  • 当单个的链表的结点个数添加之前达到7,并且table的长度达到64,才会树化。
  • 当单个的链表的结点个数添加之前达到7,table的长度未达到64,先扩容。

③table[i]下面已经是树,单独处理,直接把新的映射关系连接到树的叶子结点
④添加后,size达到threshold,还要扩容
一旦扩容,就会调整所有映射关系的位置

单元测试下进行Debug

下面对put方法进行一步一步的代码调试跟踪

单元测试的例子

我这里在单元测试下,对map添加15对映射关系,key是Integer的类型(0-14),而value可以一样在这里插入图片描述这里解释一下,为啥Integer的hashCode就是本身数值,我们打开Integer的源码查看
**ctrl+shift+T**输入Integer打开Java.lang下的Integer打开源码,找到Outline下的hashCode
两个重载方法
在这里插入图片描述

开始调试

给put方法那里双击添加断点,然后选中单元测试方法右击进行debugs as
看第一次的初始化,map里面啥也没有
在这里插入图片描述然后点执行下一步进入循环,这里点了两次,执行了两次循环
在这里插入图片描述
当执行了12次时,理论上达到了阈值,map的size应该会翻倍,操作后如图显示
在这里插入图片描述
显示map的容量size翻倍了

猜你喜欢

转载自blog.csdn.net/BlackBtuWhite/article/details/107715159