目录
- 1、哈希表
- 2、什么是 HashMap?
- 3、HashMap定义
- 4、字段属性
- 5、构造函数
- 6、确定哈希桶数组索引位置
- 7、添加元素
- 8、扩容机制
- 9、删除元素
- 10、查找元素
- 11、遍历元素
- 12、总结
本篇博客我们来介绍在 JDK1.8 中 HashMap 的源码实现,这也是最常用的一个集合。但是在介绍 HashMap 之前,我们先介绍什么是 Hash表。
1、哈希表
Hash表也称为散列表,也有直接译作哈希表,Hash表是一种根据关键字值(key - value)而直接进行访问的数据结构。也就是说它通过把关键码值映射到表中的一个位置来访问记录,以此来加快查找的速度。在链表、数组等数据结构中,查找某个关键字,通常要遍历整个数据结构,也就是O(N)的时间级,但是对于哈希表来说,只是O(1)的时间级。
比如对于前面我们讲解的 ArrayList 集合和 LinkedList ,如果我们要查找这两个集合中的某个元素,通常是通过遍历整个集合,需要O(N)的时间级。
如果是哈希表,它是通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表,只需要O(1)的时间级。
①、存放在哈希表中的数据是key-value 键值对,比如存放哈希表的数据为:
{Key1-Value1,Key2-Value2,Key3-Value3,Key4-Value4,Key5-Value5,Key6-Value6}
如果我们想查找是否存在键值对 Key3-Value3,首先通过 Key3 经过散列函数,得到值 k3,然后通过 k3 和散列表对应的值找到是 Value3。
②、当然也有可能存放哈希表的值只是 Value1,Value2,Value3这种类型:
{Value1,Value2,Value3,Value4,Value5,Value6}
这时候我们可以假设 Value1 是等于 Key1的,也就是{Value1-Value1,Value2-Value2,Value3-Value3,Value4-Value4,Value5-Value5,Value6-Value6}可以将 Value1经过散列函数转换成与散列表对应的值。
大家都用过汉语字典吧,汉语字典的优点是我们可以通过前面的拼音目录快速定位到所要查找的汉字。当给定我们某个汉字时,大脑会自动将汉字转换成拼音(如果我们认识,不认识可以通过偏旁部首),这个转换的过程我们可以看成是一个散列函数,之后在根据转换得到的拼音找到该字所在的页码,从而找到该汉字。
汉语字典是哈希表的典型实现,但是我们仔细思考,会发现这样几个问题?
①、为什么要有散列函数?
②、多个 key 通过散列函数会得到相同的值,这时候怎么办?
对于第一个问题,散列函数的存在能够帮助我们更快的确定key和value的映射关系,试想一下,如果没有汉字和拼音的转换规则(或者汉字和偏旁部首的),给你一个汉字,你该如何从字典中找到该汉字?我想除了遍历整部字典,你没有什么更好的办法。
对于第二个问题,多个 key 通过散列函数得到相同的值,这其实也是哈希表最大的问题——冲突。比如同音字汉字,我们得到的拼音就会是相同的,那么我们该如何在字典中存放同音字汉字呢?有两种做法:
第一种是开放地址法,当我们遇到冲突了,这时候通过另一种函数再计算一遍,得到相应的映射关系。比如对于汉语字典,一个字 “余”,拼音是“yu”,我们将其放在页码为567(假设在该位置),这时候又来了一个汉字“于”,拼音也是“yu”,那么这时候我们要是按照转换规则,也得将其放在页码为567的位置,但是我们发现这个页码已经被占用了,这时候怎么办?我们可以在通过另一种函数,得到的值加1。那么汉字"于"就会被放在576+1=577的位置。
第二种是链地址法,我们可以将字典的每一页都看成是一个子数组或者子链表,当遇到冲突了,直接往当前页码的子数组或者子链表里面填充即可。那么我们进行同音字查找的时候,可能需要遍历其子数组或者子链表。如下图所示:
对于开放地址法,可能会遇到二次冲突,三次冲突,所以需要良好的散列函数,分布的越均匀越好。对于链地址法,虽然不会造成二次冲突,但是如果一次冲突很多,那么会造成子数组或者子链表很长,那么我们查找所需遍历的时间也会很长。
关于哈希表的详细介绍,请点击这里。
2、什么是 HashMap?
听名字就知道,HashMap 是一个利用哈希表原理来存储元素的集合。遇到冲突时,HashMap 是采用的链地址法来解决,在 JDK1.7 中,HashMap 是由 数组+链表构成的。但是在 JDK1.8 中,HashMap 是由 数组+链表+红黑树构成,新增了红黑树作为底层数据结构,结构变得复杂了,但是效率也变的更高效。下面我们来具体介绍在 JDK1.8 中 HashMap 是如何实现的。
3、HashMap定义
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射,而且 key 和 value 都可以为 null。
1 public class HashMap<K,V> extends AbstractMap<K,V> 2 implements Map<K,V>, Cloneable, Serializable {
首先该类实现了一个 Map 接口,该接口定义了一组键值对映射通用的操作。储存一组成对的键-值对象,提供key(键)到value(值)的映射,Map中的key不要求有序,不允许重复。value同样不要求有序,但可以重复。但是我们发现该接口方法有很多,我们设计某个键值对的集合有时候并不像实现那么多方法,那该怎么办?
JDK 还为我们提供了一个抽象类 AbstractMap ,该抽象类继承 Map 接口,所以如果我们不想实现所有的 Map 接口方法,就可以选择继承抽象类 AbstractMap 。
但是我们发现 HashMap 类即继承了 AbstractMap 接口,也实现了 Map 接口,这样做难道不是多此一举?后面我们会讲的 LinkedHashSet 集合也有这样的写法。
毕竟 JDK 经过这么多年的发展维护,博主起初也是认为这样是有具体的作用的,后来找了很多资料,发现这其实完全没有任何作用,具体出处。
据 java 集合框架的创始人Josh Bloch描述,这样的写法是一个失误。在java集合框架中,类似这样的写法很多,最开始写java集合框架的时候,他认为这样写,在某些地方可能是有价值的,直到他意识到错了。显然的,JDK的维护者,后来不认为这个小小的失误值得去修改,所以就这样存在下来了。
HashMap 集合还实现了 Cloneable 接口以及 Serializable 接口,分别用来进行对象克隆以及将对象进行序列化。
4、字段属性
1 //序列化和反序列化时,通过该字段进行版本一致性验证 2 private static final long serialVersionUID = 362498820763181265L; 3 //默认 HashMap 集合初始容量为16(必须是 2 的倍数) 4 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 5 //集合的最大容量,如果通过带参构造指