HashMap源码学习笔记
来源:互联网 发布:qq飞车通灵剃刀数据 编辑:程序博客网 时间:2024/05/22 06:12
理解HashMap的关键,在于理解它底层的数据结构,查找、增加、删除元素的方法,为了理解这些方法,就需要理解Hash函数的原理,HashMap如何触发自动扩容,以及如何解决散列冲突。
本文试图从上述几个关键问题说起来分享一下HashMap源码学习的过程。
源码中的doc
HashMap大致上跟HashTable相同,但是HashMap是非线程安全的,而且支持Null Key和Null Value.
当有多个线程同时访问HashMap时,若其中存在线程对HashMap进行modify structurally(包括put、remove操作,不包括set、get操作),那么就需要考虑线程同步的HashMap,比如synchronizedMap或者concurrentHashMap。
多线程访问导致HashMap发生死锁的一个案例见本博客另一篇博文
HashMap不能保证插入顺序.
HashMap迭代遍历所有元素的时间复杂度与HashMap的capacity和size之和成正比,所以如果迭代的性能要求较高,就要考虑INITIAL_CAPACITY不能设置过高,LOAD_FACTOR不能设置过低。
//下面是Java HashMap中的默认值 /** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 16; /** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f;
HashMap的Size、Capacity、Load_Factor、Threshold说明 1
把HashMap抽象成多个用数组形式组织起来的bucket,每个bucket装着的是映射到同一个hashcode位置的KV们
那么bucket的个数就是Capacity
KV的个数就是Size
Load_Factor就是装载因子,用来衡量HashMap的装载情况
Threshold是扩容的阈值,在插入新元素时若发现插入后Size>=(Threshold=Capacity*Load_Factor),则会对HashMap进行扩容(Capacity*2)并将现有元素重新插入到新的HashMap中。
回归正题,理解了HashMap中这几个重要概念之后,就能理解为了良好的权衡时间和空间成本,LOAD_FACTOR的设置比较重要,过高会降低空间成本但增加查找成本,建议使用默认设置。
另外,如果对HashMap中存储的KV对的数量有个比较准确的估计,那么需要合理的设置Capacity的值,尽量减少扩容和Rehash的情况发生。
HashMap底层的数据结构
这里有几个关键的类,一个是HashMap本身,还有一个是Entry。java优秀封装性的一个体现在于容器类,HashMap也是一个容器类,我们可以在里面装入各种类型的KV对,无论K,V本身的类型。这是通过将KV封装成了一个Entry类来做到的。
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; public final int hashCode() { return (key==null ? 0 : key.hashCode()) ^ (value==null ? 0 : value.hashCode()); } public final String toString() { return getKey() + "=" + getValue(); }
HashMap在底层维护了一个数组table,来存放这些Entry。
我们关注一下所有Entry都有一个属性为next,这是为了解决IndexFor冲突问题,在每一个table元素的位置实际上引入了一个链表结构,所有映射成相同Index的Entry将通过next指针组织放在同一条链表中。
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); table = new Entry[DEFAULT_INITIAL_CAPACITY]; init(); }
HashMap中的插入和查找操作
先从put函数说起
/** put操作 */ public V put(K key, V value) { if (key == null) //由于HashMap支持Key为Null,所以需要单独考虑Key为Null的情况 return putForNullKey(value); //key本身是一个对象,但key对象的hashCode生成方法该对象自带的,可能是jdk定义的,也可能是用户自定义的,是不可控的,可能是比较差的hash函数,所以需要再进行一次hash,根据这个二次hash值来定位应该把当前Entry放到HashMap的第i个bucket中 int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); //将该Entry插入第i个bucket维护的链表结尾 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; //如果该Entry已存在,返回旧值 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } //如果是新插入的Entry,将HashMap的版本码++,并将该Entry加入到链表结尾 modCount++; addEntry(hash, key, value, i); return null; } void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //在插入时会判断是否需要扩容 if (size++ >= threshold) resize(2 * table.length); }
在put的过程中,有两个很重要的函数需要理解一下,分别是hash和indexfor。
hash的作用在前文已说明,是为了防御Key对象拥有的可能性能较差的hash函数。
当我们拥有了一个Key的hashCode,如何根据这个hashCode将这个Entry映射到数量为Capacity的某个bucket中去呢?
hashCode是一个32位的Int数,可表示的大小从-2147483648到2147483648,如果HashMap能有这么大的容量,那么根据HashCode的性质,若Key不同,则根本不存在冲突的可能。
当然不可能这么做。
比较容易想到的方法是将hashCode根据数组长度来取模。
然而取模运算比较低效,我们只需要达成一个目的,那就是hashCode会被映射为一个小于Capacity的值,并尽量均匀分布。具体做法见indexFor函数。
indexFor函数实际上只利用了HashCode的低n位来做散列,这里为了防止映射冲突,有两个trick2:
1、Capacity需得是2的n次方幂;
2、hash优化函数将HashCode的每个四位都做了一次异或,意在混合原始哈希码的各个部分,以此来加大低位的随机性
/** HashMap中定义的Hash方法,Null始终被映射为0 */ static int hash(int h) { //java1.7 h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); //java1.8中简化了这个过程,具体见文末相关文章 } /** * Returns index for hash code h. */ static int indexFor(int h, int length) { return h & (length-1); }
理解了put操作,get操作就简单了
public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
相关文章
- HashMap中capacity、loadFactor、threshold、size等概念的解释 ↩
- 知乎:JDK 源码中 HashMap 的 hash 方法原理是什么? ↩
- HashMap源码学习笔记
- HashMap源码学习笔记
- HashMap和HashTable源码学习笔记
- HashMap、HashSet、Hashtable源码学习笔记
- HashMap源码解析——学习笔记
- HashMap 源码笔记
- HashMap源码阅读笔记
- HashMap源码分析笔记
- HashMap源码解析笔记
- HashMap源码阅读笔记
- HashMap源码于都笔记
- HashMap源码解析笔记
- HashMap源码阅读笔记
- HashMap源码阅读笔记
- HashMap源码学习
- HashMap源码学习
- HashMap源码学习
- JAVA源码学习-HashMap
- nginx 跨域访问
- set find erase 自定义排序类型
- [时间序列分析][1]--平稳性,白噪声的检验
- 题目1519:合并两个排序的链表
- Java代码的 MD5 / SHA-1 等Hash算法的加密
- HashMap源码学习笔记
- Spring4深入理解----事务(声明式事务和xml配置事务,事务传播属性,事务其他属性(隔离级别&回滚&只读&过期))
- C# 输入法
- python中coding:utf-8和setdefaultencoding区别
- python学习笔记4
- java数组初始化以及数组拷贝
- cc: 错误:firstc.c:没有那个文件或目录 cc: 致命错误:没有输入文件 编译中断。
- React内置类型
- 性能测试基础知识(概念)