初认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集合系列一:HashMap源码解读的更多相关文章

  1. java集合系列之HashMap源码

    java集合系列之HashMap源码 HashMap的源码可真不好消化!!! 首先简单介绍一下HashMap集合的特点.HashMap存放键值对,键值对封装在Node(代码如下,比较简单,不再介绍)节 ...

  2. Java集合系列[3]----HashMap源码分析

    前面我们已经分析了ArrayList和LinkedList这两个集合,我们知道ArrayList是基于数组实现的,LinkedList是基于链表实现的.它们各自有自己的优劣势,例如ArrayList在 ...

  3. Java集合系列[4]----LinkedHashMap源码分析

    这篇文章我们开始分析LinkedHashMap的源码,LinkedHashMap继承了HashMap,也就是说LinkedHashMap是在HashMap的基础上扩展而来的,因此在看LinkedHas ...

  4. java集合系列之LinkedList源码分析

    java集合系列之LinkedList源码分析 LinkedList数据结构简介 LinkedList底层是通过双端双向链表实现的,其基本数据结构如下,每一个节点类为Node对象,每个Node节点包含 ...

  5. java集合系列之ArrayList源码分析

    java集合系列之ArrayList源码分析(基于jdk1.8) ArrayList简介 ArrayList时List接口的一个非常重要的实现子类,它的底层是通过动态数组实现的,因此它具备查询速度快, ...

  6. 【Java集合学习】HashMap源码之“拉链法”散列冲突的解决

    1.HashMap的概念 HashMap 是一个散列表,它存储的内容是键值对(key-value)映射. HashMap 继承于AbstractMap,实现了Map.Cloneable.java.io ...

  7. Java集合系列:-----------03ArrayList源码分析

    上一章,我们学习了Collection的架构.这一章开始,我们对Collection的具体实现类进行讲解:首先,讲解List,而List中ArrayList又最为常用.因此,本章我们讲解ArrayLi ...

  8. Java集合系列[1]----ArrayList源码分析

    本篇分析ArrayList的源码,在分析之前先跟大家谈一谈数组.数组可能是我们最早接触到的数据结构之一,它是在内存中划分出一块连续的地址空间用来进行元素的存储,由于它直接操作内存,所以数组的性能要比集 ...

  9. java集合中的HashMap源码分析

    1.hashMap中的成员分析 transient Node<K,V>[] table; //为hash桶的数量 /** * The number of key-value mapping ...

  10. Java集合系列[2]----LinkedList源码分析

    上篇我们分析了ArrayList的底层实现,知道了ArrayList底层是基于数组实现的,因此具有查找修改快而插入删除慢的特点.本篇介绍的LinkedList是List接口的另一种实现,它的底层是基于 ...

随机推荐

  1. Hibernate第一天——入门和基本操作

    第一个接触的框架就是这个Hibernate框架了,Hibernate本意是 冬眠 ,这里有必要引用CSDN上某位网友某个帖子的评论先引出框架的概念: 框架:一个软件半成品,帮你做了一些基础工作,你就可 ...

  2. WPF RichTextBox自动调整高度

    原文:WPF RichTextBox自动调整高度 大概两年前的这个时间段,当时做项目遇到了一个问题:环境VS2005.WinForm,需要RichTextBox根据内容自动调整高度.当时用了各种方法都 ...

  3. 【BZOJ1054】[HAOI2008]移动玩具

    [BZOJ1054][HAOI2008]移动玩具 题面 bzoj 洛谷 题解 太\(sb\)了,不想写了,直接点开洛谷题面单击右边蓝色按钮题解即可

  4. AGC 007 D - Shik and Game

    D - Shik and Game 链接 题意: 数轴上有一个人,从0出发到E,速度为1.数轴上还有n只熊,每只熊会在经过后的T时刻后产生一个金币.给定E,T以及n个熊的坐标pi,求收集完所有金币并到 ...

  5. codeforces 1053D 树形DP

    题意:给一颗树,1为根节点,有两种节点,min或者max,min节点的值是它的子节点的值中最小的,max节点的值是它的子节点的值中最大的,若共有k个叶子,叶子的值依次为1~k. 问给每个叶子的值赋为几 ...

  6. 洛咕P3250 [HNOI2016]网络 整体二分

    这题太神仙了必须写博客... 显然可以想到二分答案.二分一个答案mid,如果所有长度\(\geq mid\)的路径都过x,那么答案一定\(<mid\),否则答案\(\geq mid\). 那么就 ...

  7. 新买的orico蓝牙usb连接器使用方法与驱动

    因为买的型号是 BTA-403 ,所以需要下载该型号驱动 安装好后,可能会出现找不到蓝牙设备问题,所以需要重启机器,并且手动将pc蓝牙连接到手机蓝牙,然后手机蓝牙再连接蓝牙耳机,此时蓝牙耳机会显示连接 ...

  8. webpack vue app.js自动注入页面.为app.js增加随机参数

    node_modules/html-webpack-plugin/index.js 搜索 postProcessHtml 修改代码增加如下: if (assetTags && asse ...

  9. Git工具使用小结

    昨天开始看一套java接口自动化的视频,今天看到的一章是关于git这个工具使用的,上大学那会用过svn作为版本管理工具,包括现在所在的公司,用的也还是svn进行管理.其实老早就听闻过Git,Githu ...

  10. redmine on centos

    一 前言 前前后后搭建redmine,花费了很多时间.期间会遇到各种坑,因此总结下自己的方法,分享给各位童鞋. 二 操作系统  centos release 6.9 详细信息如下图:   三 安装步骤 ...