HashMap源码分析
对 HashMap
源码的简单分析。
基本的数据结构有数组,链表,树,图。
数组的特点是长度固定,空间连续,占用内存很大,空间复杂度是O(n),但是寻址容易,时间复杂度O(1).
总结:寻址容易,插入删除困难。
链表的特点是长度可变,存储空间离散,占用内存小,空间复杂度是O(1),但是寻址困难,时间复杂度为O(n).
总结:寻址困难,插入删除容易。
哈希表基于数组结构,采用键值的方式存储数据,插入删除容易,寻址是根据键值直接查找数据,时间复杂度是O(1),但是使用哈希值存储数据总会出现哈希冲突,解决哈希冲突的方法主要有,开放定址法(再散列),再哈希法,链地址法(拉链法),建立公共溢出区。
缺点:
存储空间填满时需要将其复制到另一个更大的数组结构中,并进行再哈希计算,这是哈希表内存占用的暴涨点,这是基于数组结构的一个缺点。
哈希表不能以一种特定的顺序遍历数据结构中的所有数据,如果需要按序存储,使用哈希表并不合适。
介绍
HashMap是线程不安全的HashTable是线程安全的。
HashMap就是基于链地址法实现的数据存储。也就是链表的数组。
HashMap允许键 值 为null。HashTable是不允许的。
在Android和Java里面对HashMap的实现,稍有不同,开始我还以为是我的jdk有问题。
在HashMap的基础数组中,每一个数组项称之为一个桶,HashMap就是基于这种桶+链表的结构。
存储方式,使用key值取哈希,一般的算法是hash(key)/len获得他存放位置的下标,这样就将key与下标对应起来。
存储的数据结构
- 总结来说,HashMap就是数组+内部类(Entry)实现,Entry中具有下一个Entry的引用,由此构成了链表的结构。第一行的代码是用来对HashMap进行操作时使用的变量,比如迭代HashMap所有的子项,添加一个EntrySet到HashMap中,删除,修改。。。。
1 | private transient Set<Entry<K, V>> entrySet; |
HashMapEntry
HashMap中有一个很重要的存储结构HashMapEntry,用来保存键值对,和下一个Entry的引用,这就形成了一个链表结构。
在Java中,这个静态内部类叫Entry
,在android中叫HashMapEntry ,在功能上应该是相似的。是一个存储数据的基础bean,关注一下源码:
1 | static class HashMapEntry<K, V> implements Entry<K, V> { |
哈希值的计算
存储数据是根据key的哈希值来存储的,类似这样的方法hash(key.hashcode())
Java和Android中对求哈希值的操作相似但是哈希算法不同,当进行存取操作时,key都需要使用这个算法进行中转。
在Android中使用的是Collections类下的静态方法及进行哈希计算,但是这里的h是key的hashcode(),看注释使用的是Wang/Jenkins哈希算法的变体,源代码:
1 | //java实现 |
1 | //android实现 |
put方法
检查键值是否为空,null时将存储到forNullEntry所在的链表中。
获取哈希值之后获得下标索引,检查key是否已经存在,是则替换,否则添加到当前链表,基本思想是这样的,几个不同的点
putForNullKey(value)方法,在Android和Java中都有实现,当键值为null时,分配一个数组的一项,这个数组指向key == null 的Entry
一个不同点,Java中是在添加Entry之后重新计算容量,而Android是在AddNewEntry之前进行。
扩容的方法都是扩大为原来的两倍。
在Android中的实现基本类似,使用Collections静态方法进行二次哈希计算,计算下表索引
1 | //在java中的实现代码: |
1 | //android中的实现 |
get方法
- 同样的获取hashcode,再哈希计算哈希值,获得下标索引,得到对应链表,遍历链表得到结果,在Java和Android中实现大同小异.
1 | //java中的实现 |
1 | //Android中的实现 |
初始化
- 在Java和Android中HashMap的初始化略有不同,Android中初始容量是2,Java中初始容量是16。
Java实现
- Java中的初始容量和负载因子是学习Java比较经典的内容,看一下Java中HashMap的默认构造方法
1 | static final int DEFAULT_INITIAL_CAPACITY = 16; |
- 带有参数的构造方法实现
1 | //指定容量和负载因子时会检查容量和负载因子的值是不是有问题 |
在Android中的实现
1 | //默认的初始最小容量是4,一个静态的初始数组的容量是最小容量无符号右移1位,也就是2,默认构造时,会将该静态数组交给table,这里有一行源码的注释( Forces first put invocation to replace EMPTY_TABLE),意思是根据第一次put方法加入的数量来扩充数组,此时负载容量是-1,添加时会立刻扩充容量,这可能也是为什么Android中添加元素是先检查扩充容量再添加的原因。。在put时第一次百分百检测到容量不够,此时进行一次doubleCapacity()将容量加倍,在这个方法中会调用 |
更多
JDK1.8在容量过大时使用红黑树存储数据,事件复杂度O(logn),对于红黑树不是很了解,算法很菜,暂时放放吧。
阈值 = 容量 负载因子,当容量超过阈值时会进行扩容和再散列,此时是hashmap的内存占用的增长点,扩容会将容量扩大为原来的两倍然后将数据拷贝到新的区域进行再散列。但是这里有个问题不太懂:HashMap扩容,每次添加一个元素size++,当size>阈值(容量负载因子)时进行扩容,但是HashMap是基于数组+链表的,添加的元素不一定集中到一个数组项。举个例子说,当一个hashmap容量是16,阈值就是12,此时添加的元素如果有12个了,但是却只是集中在某几个数组项所在 的链表中,那么此时进行扩容合适吗?暂时合理的解释是hash()算法可以很好的将数据分散在所有的数组项中,那么这种比较也是比较合理的。
为什么要将初始容量计算得到大于该容量的最小的2的幂?我们可以看一下这个代码
int index = hash & (tab.length - 1);
在java中有相同的设计是一个函数indexfor(int hash)
在计算hashcode对应的数组下标时使用hash&len-1代替了hash%len,只有在len是2的幂时,这两个方法才是等价不等效的。可能不太好理解,举个例子:len是16时,二进制10000,len-1是01111,如果得到的hash小于16时,比如是3吧,进行&运算01111&00011得到的结果是3,也就是本身。确实等价于3%16,再比如大于16,是19吧,再大也是一个意思,也就是01111&10011得到的结果是3,等价于19%16,右数第5位以上的1都被过滤掉了,说这么多只是解释一下确实hash&len-1等价于hash%len,但是使用位运算效率会大大提高,在源码中位运算随处可见。ps:我总觉的还有别的作用,但是没有了解到。
目前维护的几个项目,求 ✨✨✨✨
- SocialSdk 登录分享功能原生接入
- LightAdapter 轻量级适配器
- ImageEditor 图片处理,裁剪旋转,贴纸涂鸦,滤镜等
- WeexCube Weex 容器方案
- Kotlin 学习系列总结,共计 22 篇
- 本文链接: http://cdevlab.top/article/1753510098/
- 版权声明: 版权所有,转载请注明出处!