深入理解JAVA集合系列一:HashMap源码解读
2016-05-27 23:04
1071 查看
初认HashMap
基于哈希表(即散列表)的Map接口的实现,此实现提供所有可选的映射操作,并允许使用null值和null键。HashMap继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。且是不同步的,意味着它不是线程安全的。
HashMap的数据结构
在java编程语言中,最基本的结构就两种,一个是数组,另一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的。HashMap也不例外,它是一个“链表的数组”的数据结构。从下图中可以看出HashMap的底层就是一个table数组,数组的元素就是Entry。而Entry就是HashMap中的一个存储单元。Entry的数据结构
table数组中的每个元素都是一个由Entry组成的单向链表,理解这句话对理解HashMap非常重要。现在来看下单向链表上一个Entry的数据结构以及源码对其的定义
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash;
HashMap的底层实现
1、存储数据
首先从我编写的这段代码开始,开启HashMap的源码解析之路:public static void main(String[] args) { Map<String,String> hashMap = new HashMap<String,String>(); hashMap.put("语文","89"); hashMap.put("数学","95"); hashMap.put("英语","88"); }
在第3行,创建了一个HashMap
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); table = new Entry[DEFAULT_INITIAL_CAPACITY]; init(); }
其中初始容量DEFAULT_INITIAL_CAPACITY为16,loadFactor(负载因子)为0.75。也就是说HashMap在创建的时候构造了一个大小为16的Entry数组。Entry内所有的数据都采用默认值null。
接下来看put方法底层实现是如何的:
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
1、在2、3两行上,可以看出HashMap允许key值为null的存在,并且存放在数组中下标为0的位置上。
2、在第4、5行上,对key的hashCode重新计算hash值,然后通过hash值作为下标,定位到table数组中的位置。在这里需要强调的是,不同的key最终得到的hash值可能会相同,这就是我们经常听说的“hash碰撞"。这也就意味着,table数组同一个位置上需要存放不同的Entry,这也就是会有单向链表的原因了。
3、下面我们来看下Entry单向链表是如何实现的:Entry类中有一个next属性,作用是指向下一个Entry。举个例子,第一个键值A进来,通过计算其key的hash值得到的index=4,记做Entry[0]=A。一会又来了一个键值B,计算得到的index也等于4,这时候怎么办?HashMap会这样做,B.next=A,Entry[0]=B。如果有进来C,index也等于4,那么情况就会变成这样,C.next=B,B.next=A,Entry[0]=C。最后我们会发现,在index=4的位置上,存储了ABC三个键值,它们通过next这个属性链接在一起。所以数组中存取的是最后插入的元素。
4.第6-14行,hash值定位到数组的某个元素上,然后对这个元素中的Entry单向链表做遍历,先对比hash值,在对比key值。如果相同,则覆盖并返回原有的value值,不执行后面的代码。
5、第16行的作用是用于fail-fast机制,每次修改hashMap数据结构的时候这个值都会自增。我们都知道HashMap是线程非安全的,如果在使用迭代器过程中有其他线程修改了map,将会抛出ConcurrentModificaitonException。这就是所谓的fail-fast的策略。
6、下面17行就是非常关键的addEntry:
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); }
第2行中e表示这个索引位置上现有的Entry,即在这之前最后插入的元素,如果这个位置上没有元素,则e为null。
第3行中,以本次新增的键值为属性创建了新的Entry,并且next指向e,这样就完成了put的动作。
第4、5两行就是在新增了元素之后,判断table数组是否需要扩容。一般是扩容现有容量的一倍。
2、删除数据
final Entry<K,V> removeEntryForKey(Object key) { int hash = (key == null) ? 0 : hash(key.hashCode()); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev; while (e != null) { Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; }
1、首先如果key为null,则返回的数组索引为0.
2、在4、5两行上,prev表示待删除的Entry前一个位置的Entry,e表示待删除的Entry。这个时候,可以看到prev=e,且是该该位置上第一个Entry。
3、7-23行上,如果第一次就满足了条件判断,则表示链表头上的Entry就是需要删除的Entry,且prev一定等于e,则table[i]就直接指向table[i]的下一个节点。
如果第一次条件没有满足,说明链表头上的Entry不是这次需要删除的Entry。那么接下来就会有一个很优雅的设计:
prev = e; e = next;
将e赋值给prev,而e表示下一个元素,然后再进行while循环做前面的一系列判断。当执行到17行的时候,e的前一个Entry(就是prev)的next直接执行e的后一个节点next,这样e就从这个单向链表中消失,e的前后Entry链接到一起了。
3、hashCode的作用
在HashMap提供的方法中,都会对key的hashCode做hash算法。那究竟这样有什么作用呢?hash算法主要防止生成的index大量重复,就是前面提到的hash碰撞。如果index大量重复,就是导致同一个位置上的Entry会有多个,而有些位置上的entry没有。这样会严重影响性能:
如果10key,其中5个最终得到的index相同。这就意味着有一个位置上有5个Entry,另外5个均匀分布。所以在做存取或者删除的时候,就会反复执行这段代码:
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
而在设计HashMap的时候,是希望元素能够随机均匀的分布在数组中,这样才能提升HashMap的效率。
相关文章推荐
- Java BASE64加密解密
- [javaSE] 集合工具类(Collections-sort)
- Servlet
- eclipse配置并使用svn
- Spring-EL表达式
- Spring-bean构造方法
- JAVA反射机制(二)
- Spring Data JPA 1.10.1 详解一之Spring Data JPA介绍
- java中创建线程主要有三种方式
- java bean
- java jni 开发
- 相对准确的java程序性能测试实现方案
- 基于spring3.0的springmvc的ajax数据请求
- java 异常练习题
- Java类加载及实例化的调用顺序
- springboot
- spring 声明式事务原理解读
- Java动态代理机制研读
- JAVA_JDBC
- 简要理解java里的多线程