Java集合源码学习(四)HashMap
一、数组、链表和哈希表结构
数据结构中有数组和链表来实现对数据的存储,这两者有不同的应用场景,
数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,插入和删除容易;
哈希表的实现结合了这两点,哈希表的实现方式有多种,在HashMap中使用的是链地址法,也就是拉链法。
拉链法实际上是一种链表数组的结构,由数组加链表组成,在这个长度为16的数组中(HashMap默认初始化大小就是16),每个元素存储的是一个链表的头结点。
通过元素的key的hash值对数组长度取模,将这个元素插入到数组对应位置的链表中。
例如在图中,337%16=1,353%16=1,于是将其插入到数组位置1的链表头结点中。
二、关于HashMap
(1)继承和实现
继承AbstractMap抽象类,Map的一些操作在AbstractMap里已经提供了默认实现,
实现Map接口,可以应用Map接口定义的一些操作,明确HashMap属于Map体系,
Cloneable接口,表明HashMap对象会重写java.lang.Object#clone()方法,HashMap实现的是浅拷贝(shallow copy),
Serializable接口,表明HashMap对象可以被序列化
(2)内部数据结构
HashMap的实际数据存储在Entry类的数组中,
上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[]。
/*** 内部实际的存储数组,如果需要调整,容量必须是2的幂*/transient Entry[] table;
再来看一下Entry这个内部静态类,
static class Entry<K,V> implements Map.Entry<K,V> {final K key;//Key-value结构的keyV value;//存储值Entry<K,V> next;//指向下一个链表节点final int hash;//哈希值/*** Creates new entry.*/Entry(int h, K k, V v, Entry<K,V> n) {value = v;next = n;key = k;hash = h;}......}
(3)线程安全
HashMap是非同步的,即线程不安全,在多线程条件下,可能出现很多问题,
1.多线程put后可能导致get死循环,具体表现为CPU使用率100%(put的时候transfer方法循环将旧数组中的链表移动到新数组)
2.多线程put的时候可能导致元素丢失(在addEntry方法的new Entry<K,V>(hash, key, value, e),如果两个线程都同时取得了e,则他们下一个元素都是e,然后赋值给table元素的时候有一个成功有一个丢失)
关于HashMap线程安全性更多的了解参考相关的网上资源,这里不多叙述。
需要线程安全的哈希表结构,可以考虑以下的方式:
使用Hashtable 类,Hashtable 是线程安全的;
使用并发包下的java.util.concurrent.ConcurrentHashMap,ConcurrentHashMap实现了更高级的线程安全;
或者使用synchronizedMap() 同步方法包装 HashMap object,得到线程安全的Map,并在此Map上进行操作。
如:
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {return new SynchronizedMap<>(m);}
三、常用方法
(1)Map接口定义的方法
HashMap可以应用所有Map接口定义的方法:
public interface Map<K,V> {public static interface Entry<K,V> {//获取该Entry的key public abstract Object getKey();//获取该Entry的valuepublic abstract Object getValue();//设置Entry的value public abstract Object setValue(Object obj);public abstract boolean equals(Object obj);public abstract int hashCode();}//返回键值对的数目 int size();//判断容器是否为空 boolean isEmpty();//判断容器是否包含关键字key boolean containsKey(Object key);//判断容器是否包含值value boolean containsValue(Object value);//根据key获取value Object get(Object key);//向容器中加入新的key-value对 Object put(Object key, Object value);//根据key移除相应的键值对 Object remove(Object key);//将另一个Map中的所有键值对都添加进去 void putAll(Map<? extends K, ? extends V> m);//清除容器中的所有键值对 void clear();//返回容器中所有的key组成的Set集合 Set keySet();//返回所有的value组成的集合 Collection values();//返回所有的键值对 Set<Map.Entry<K, V>> entrySet();//继承自Object的方法boolean equals(Object obj);int hashCode();
}
(2)构造方法
HashMap使用Entry[] 数组存储数据,
另外维护了两个非常重要的变量:initialCapacity(初始容量)、loadFactor(加载因子)。
初始容量就是初始构造数组的大小,可以指定任何值,
但最后HashMap内部都会将其转成一个大于指定值的最小的2的幂,比如指定初始容量12,但最后会变成16,指定16,最后就是16。
加载因子是控制数组table的饱和度的,默认的加载因子是0.75,
DEFAULT_LOAD_FACTOR = 0.75f;
也就是数组达到容量的75%,就会自动的扩容。
另外,HashMap的最大容量是2^30,
static final int MAXIMUM_CAPACITY = 1 << 30;
默认的初始化大小是16,
static final int DEFAULT_INITIAL_CAPACITY = 16;
HashMap提供了四种构造方法,可以使用默认的容量等进行初始化,
也可以显式制定大小和加载因子,还可以使用另外的map进行构造和初始化。
public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR;threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);table = new Entry[DEFAULT_INITIAL_CAPACITY];init();}public HashMap(Map<? extends K, ? extends V> m) {this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);putAllForCreate(m);}public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}public HashMap(int initialCapacity, float loadFactor) {......}
四、解决哈希冲突的办法
(1)什么是哈希冲突
理论上哈希函数的输入域是无限的,优秀的哈希函数可以将冲突减少到最低,但是却不能避免,下面是一个典型的哈希冲突的例子:
用班级同学做比喻,现有如下同学数据
张三,李四,王五,赵刚,吴露.....
假如我们编址规则为取姓氏中姓的开头字母在字母表的相对位置作为地址,则会产生如下的哈希表
位置 字母 姓名 0 a 1 b 2 c ...
10 L 李四 ...
22 W 王五,吴露 ..
25 Z 张三,赵刚 我们注意到,灰色背景标示的两行里面,关键字王五,吴露被编到了同一个位置,关键字张三,赵刚也被编到了同一个位置。老师再拿号来找张三,座位上有两个人,"你们俩谁是张三?"(这段描述很形象,引用自hash是如何处理冲突的?)
(2)解决哈希冲突的方法
常见的办法开放定址法,再哈希法,链地址法以及建立一个公共溢出区等,这里只考察链地址法。
链地址法就是最开始我们提到的链表-数组结构,
将所有关键字为同义词的记录存储在同一线性链表中。
五、源码分析
(1)HashMap的存取实现
HashMap的存取主要是put和get操作的实现。
执行put方法时根据key的hash值来计算放到table数组的下标,
如果hash到相同的下标,则新put进去的元素放到Entry链的头部。
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;}
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支持key=null的情况,看这个代码:
private V putForNullKey(V value) {for (Entry<K,V> e = table[0]; e != null; e = e.next) {if (e.key == null) {V oldValue = e.value;e.value = value;e.recordAccess(this);return oldValue;}}......}
(2)哈希函数
下面看一下HashMap使用的哈希函数,源码来自JDK1.6:
/*** 哈希函数* 看一下具体的操作,首先对h分别进行无符号右移20位和12位,* 然后对两个值进行按位异或,最后再与h进行按位异或,* 得到新的h后再进行一次同样的操作,分别右移7位和4位,具体的哈希函数优劣就不去研究了* 这个方法可以尽量减少碰撞*/static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);}
(3)再散列rehash过程
当哈希表的容量超过默认容量时,必须调整table的大小。
当容量已经达到最大可能值时,那么该方法就将容量调整到Integer.MAX_VALUE返回,这时,需要创建一个新的table数组,将table数组的元素转移到新的table数组中。
/*** 再散列过程* Rehashes the contents of this map into a new array with a* larger capacity. This method is called automatically when the* number of keys in this map reaches its threshold.*/void resize(int newCapacity) {Entry[] oldTable = table;int oldCapacity = oldTable.length;if (oldCapacity == MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return;}Entry[] newTable = new Entry[newCapacity];transfer(newTable);table = newTable;threshold = (int)(newCapacity * loadFactor);}/*** 把当前Entry[]表的全部元素转移到新的table中*/void transfer(Entry[] newTable) {Entry[] src = table;int newCapacity = newTable.length;for (int j = 0; j < src.length; j++) {Entry<K,V> e = src[j];if (e != null) {src[j] = null;do {Entry<K,V> next = e.next;int i = indexFor(e.hash, newCapacity);e.next = newTable[i];newTable[i] = e;e = next;} while (e != null);}}}
参考
Java HashMap的死循环
Thinking in Java之HashMap源码分析
hash是如何处理冲突的?
Java集合源码学习(四)HashMap相关推荐
- 【Java集合源码剖析】HashMap源码剖析
转载请注明出处:http://blog.csdn.net/ns_code/article/details/36034955 您好,我正在参加CSDN博文大赛,如果您喜欢我的文章,希望您能帮我投一票,谢 ...
- Java集合源码学习(五)几种常用集合类的比较
这篇笔记对几个常用的集合实现,从效率,线程安全和应用场景进行综合比较. 1.ArrayList.LinkedList与Vector的对比 (1)相同和不同 都实现了List接口,使用类似. Vecto ...
- Java集合源码学习(4)HashSet
1 概述 HashSet 是一个没有重复元素的集合.它的底层是由HashMap实现的,不保证元素的顺序.HashSet允许使用 null 元素. 截取一段源码: public class HashSe ...
- java集合源码分析之HashMap
UML类图: 基本简介: 底层的数据结构是数组,数组的元素类型是链表或者红黑树. 元素的添加可能会触发数组的扩容,会使元素重新哈希放入桶中,效率比较低. 元素在不扩容的情况下添加效率高,查找.删除.修 ...
- JDK11源码学习05 | HashMap类
JDK11源码学习05 | HashMap类 JDK11源码学习01 | Map接口 JDK11源码学习02 | AbstractMap抽象类 JDK11源码学习03 | Serializable接口 ...
- Java集合源码分析(二)ArrayList
ArrayList简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线 ...
- JAVA JDK 源码学习
JAVA JDK 源码学习 ,以1.8为例,按照下面图片顺序依次学习: applet ,awt,beans,io,lang,math,net,nio,rmi,security,sql,text,tim ...
- Java集合源码浅析(一) : ArrayList
(尊重劳动成果,转载请注明出处:https://yangwenqiang.blog.csdn.net/article/details/105418475冷血之心的博客) 背景 一直都有这么一个打算,那 ...
- java Integer 源码学习
转载自http://www.hollischuang.com/archives/1058 Integer 类在对象中包装了一个基本类型 int 的值.Integer 类型的对象包含一个 int 类型的 ...
最新文章
- 使用Python中的卷积神经网络进行恶意软件检测
- 面试结尾——你有什么问题?
- python 等比例缩放图片 自写
- r型聚类分析怎么做_【SPSS数据分析】SPSS聚类分析(R型聚类)的软件操作与结果解读 ——【杏花开生物医药统计】...
- 在JavaScript中使用json.js:访问JSON编码的某个值
- Linux服务器的初步配置流程
- 飞秋-程序的找工作之苦
- webservice python开发接口_基于Python的Webservice开发(四)-泛微OA的SOAP接口
- hdu-3488-Tour(KM最佳完美匹配)
- c语言 char转int_第三章、C语言中的数据类型
- JavaScript 小知识
- Layui实现多条件查询
- 取消word文档中某些页面的页眉
- 增大或者减小图片大小的方法
- 简易的安卓天气app(四)——搜索城市、完善页面
- java 穷举 排列组合_穷举排列组合列表
- [Sensor]LSM6DSL-加速度计、陀螺仪传感器
- RV1126RV1109 buildroot 增加QT程序
- 视频教程-跟宁哥学Go语言视频课程(10):反射-Go语言
- 安装Matlab R2022a/64位