面试必备1:HashMap(JDK1.8)原理以及源码分析

首先附上我的几篇其它文章链接感兴趣的可以看看,如果文章有异议的地方欢迎指出,共同进步,顺便点赞谢谢!!!
Android framework 源码分析之Activity启动流程(android 8.0)
Android studio编写第一个NDK工程的过程详解(附Demo下载地址)
面试必备2:JDK1.8LinkedHashMap实现原理及源码分析
Android事件分发机制原理及源码分析
View事件的滑动冲突以及解决方案
Handler机制一篇文章深入分析Handler、Message、MessageQueue、Looper流程和源码
Android三级缓存原理及用LruCache、DiskLruCache实现一个三级缓存的ImageLoader

HashMap概述:

在分析HashMap的源码前,需要了解一下几个问题。
面试必备2LinkedHashMap的源码分析(基于JDK1.8)

1:HashMap的数据结构

我们需要先知道HashMap是基于什么样的数据结构来进行数据存储的,知道了这些我们再去看源码就容易的多。

散列表(哈希表) 我们常用的数据结构就是数组和链表,数组具有增删慢查找快的特点,而链表具有增删快查找慢 的特点;基于上述特点,HashMap 即想要查询效率快,又想增删效率高,基于这样的特点HashMap的数据结构就是通过数组和链表组成的散列链表。
一直到JDK7为止,HashMap的结构都基于一个数组以及多个链表的实现,hash值冲突的时候,就将对应节点以链表的形式存储;JDK8中,HashMap采用的是数组(位桶)+链表/红黑树组成,当同一个hash值的节点数不小于8时,将不再以单链表的形式存储了,会被调整成一颗红黑树,提高查询效率,这就是JDK7与JDK8中HashMap实现的最大区别。

在这里插入图片描述

2:存储节点 : Node 和TreeNode

HashMap存储数据时如何组织出这样一个散列表呢?HashMap中将我们存入的每一个数据封装成一个Node类(Node节点),Node应该有以下下属性 :key--------键、value-----值 组成我们put的键值对,既然要组织起链表则必须有Node next属性、还有一个根据key算出的int 类型hash值,hash值用来确定该该节点所在数组的索引后面会进行详细描述。

/**
 *  散列表的节点  Node  部分源码
 */
 static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//用于确定该节点所在数组的位置,  下标
        final K key;//  我们所操作的键
        V value;//  值
        Node<K,V> next;//存储下一个节点,通过next组织起链表
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;//位置
            this.key = key;
            this.value = value;
            this.next = next;
        }
		/**
         *  返回当前节点的根节点  
         */
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }
    }

当链表长度大于8时将链表转换成红黑树,红黑树节点TreeNode部分源码

 static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> {
        TreeNode<K,V> parent;  // 父节点
        TreeNode<K,V> left;//左子树
        TreeNode<K,V> right;//右子树
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;//颜色属性  
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
}

当我们放入或取出元素时大致流程是:

  1. 现根据hash函数去求出key的Hash值,至于求hash值得算法将在后面进行详细分析
  2. 根据上一步的hash值可以确定在数组的哪个位置,定下标(数组的索引)
  3. 根据索引从数组中获取数据,如果为NULL 则将该元素直接放入或取出(直接在数组中操作),如果该位置数据不为NuLL,则需要向该元素所在的链表进行数据的操作。
哈希冲突(哈希碰撞)

如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。

3:HashMap中散列表中数组(或位桶)如何表示?

HashMap源码中与数组相关的几个成员属性:

  1. 数组的表示: transient Node<K,V>[] table;
  2. 数组的大小 : 数组的初始化需要一个指定的大小,在HashMap中 数组的大小永远是 2的幂次方(原因将在源码中分析)
  3. 数组的默认大小:static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 , 1 << 4 二进制中1左移四位就是2^4=16, 不直接写16的原因是,效率高,计算机最终读取的二进制;
  4. 数组的最大容量:static final int MAXIMUM_CAPACITY = 1 << 30//最大为2^30,使用位移操作符的原因也是效率高 ;
  5. 数组扩容因子: static final float DEFAULT_LOAD_FACTOR = 0.75f//用于确定数组扩容的临界值;
  6. 以存放数据大小:transient int size//记录 hashMap 当前存储的元素的数量;
  7. 数组扩容的临界值:int threshold;//数组已使用量size>threshold时需要扩容,threshold=数组大小*扩容因子提升效率,没有必要等到数组都用完了在进行扩容,每次扩大2倍
    **注意:数组的扩容是在当已使用量大于数组容量*扩容因子(默认0.75)**时进行扩容

4:HashMap中散列表中链表的长度限制:长度越长存储查询效率低的问题

链表越长,期查询效率越低,所以链表的长度也有一个限制,我们称为阈值 ,JDK1.8的时候,链表的长度大于阈值,将其结构改成一个红黑二叉树,以提升差值效率,同时也伴随者性能的损耗增加
HashMap源码中与链表相关的几个成员属性:

  1. 转换红黑树的阈值: static final int TREEIFY_THRESHOLD = 8;//阈值 链表越深查找存储效率都低 ,超过8以后将其转换成红黑二叉树 ,提升查找效率
  2. 转换成链表的阈值static final int UNTREEIFY_THRESHOLD = 6; //当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。
  3. static final int MIN_TREEIFY_CAPACITY = 64;//当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。

6:新的数据封装成的Node对象如何去存储:
根据key计算hashCode ----》int类型的值,hash(key)然后就知道要存在数组的那个位置

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值