Java 集合系列10之 HashMap详细介绍(源码解析)和使用示例
集合类结构
Java中的集合包含多种数据结构,如链表、队列、哈希表等。从类的继承结构来说,可以分为两大类,一类是继承自Collection接口,这类集合包含List、Set和Queue等集合类。另一类是继承自Map接口,这主要包含了哈希表相关的集合类。下面我们看一下这两大类的继承结构图:
1、List、Set和Queue
2、Map:
图中的绿色的虚线代表实现,绿色实线代表接口之间的继承,蓝色实线代表类之间的继承。
List
(1)List:我们用的比较多List包括ArrayList和LinkedList,这两者的区别也很明显,从其名称上就可以看出。ArrayList的底层的通过数组实现,所以其随机访问的速度比较快,但是对于需要频繁的增删的情况,效率就比较低了。而对于LinkedList,底层通过链表来实现,所以增删操作比较容易完成,但是对于随机访问的效率比较低。
至于Vector,它是ArrayList的线程安全版本,而Stack则对应栈数据结构。
Queue
(2)Queue:一般可以直接使用LinkedList完成,从上述类图也可以看出,LinkedList继承自Deque(双端队列),所以LinkedList具有双端队列的功能。PriorityQueue的特点是为每个元素提供一个优先级,优先级高的元素会优先出队列。
Set
Set与List的主要区别是Set是不允许元素重复的,且存取是无序的(存入和取出不按照顺序),而List则可以允许元素重复的且存取有序。判断元素的重复需要根据对象的hash方法和equals方法来决定。这也是我们通常要为集合中的元素类重写hashCode方法和equals方法的原因。
HashSet:为快速查找设计的Set。存入HashSet的对象必须定义hashCode()。
TreeSet: 保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列。
LinkedHashSet:具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素插入的次序显示。
Map
Map类型的集合最大的优点在于其查找效率比较高,理想情况下可以实现O(1)的时间复杂度。
Map中最常用的是HashMap,LinkedHashMap与HashMap的区别在于前者能够保证插入集合的元素顺序与输出顺序一致。
HashMap与TreeMap的区别,与之前提到的HashSet与TreeSet的区别是一致的
HashSet和TreeSet本质上分别是通过HashMap和TreeMap来实现的
TreeSet和TreeMap存储原理
TreeSet:按特定顺序存放元素,底层结构是红黑二叉树。
红黑树算法的规则: 左小右大。
既然TreeSet可以自然排序,那么TreeSet必定是有排序规则的。
1:让存入的元素自定义比较规则。
元素自身具备比较性,需要元素实现Comparable接口,重写compareTo方法,也就是让元素自身具备比较性,这种方式叫做元素的自然排序也叫做默认排序。
2:给TreeSet指定排序规则。
当元素自身不具备比较性,或者自身具备的比较性不是所需要的。那么此时可以让容器自身具备。需要定义一个类实现接口Comparator,重写compare方法,并将该接口的子类实例对象作为参数传递给TreeMap集合的构造方法。
注意:当Comparable比较方式和Comparator比较方式同时存在时,以Comparator的比较方式为主;
当compareTo()函数返回值为0时,说明两个对象相等,此时该对象不会添加进来。
为什么使用TreeSet存入字符串,字符串默认输出是按升序排列的?
因为字符串实现了一个接口,叫做Comparable 接口.字符串重写了该接口的compareTo 方法,所以String对象具备了比较性.那么同样道理,我的自定义元素(例如Person类,Book类)想要存入TreeSet集合,就需要实现该接口,也就是要让自定义对象具备比较性.
TreeMap:TreeMap根据集合中的键进行排序
方式一:元素自身具备比较性
和TreeSet一样原理,需要让存储在键位置的对象实现Comparable接口,重写compareTo方法,也就是让元素自身具备比较性,这种方式叫做元素的自然排序也叫做默认排序。
方式二:容器具备比较性
当元素自身不具备比较性,或者自身具备的比较性不是所需要的。那么此时可以让容器自身具备。需要定义一个类实现接口Comparator,重写compare方法,并将该接口的子类实例对象作为参数传递给TreeMap集合的构造方法。
当key比较相同时,value会覆盖之前的value。
HashSet和HashMap存储原理
HashSet:元素加入Set之前需要先执行hashCode方法,如果返回的值在集合中已存在,则要继续执行equals方法,如果equals方法返回的结果也为真,则证明该元素已经存在,会将新的元素覆盖老的元素,如果返回hashCode值不同,则直接加入集合。
HashMap:
结构图
1.HashMap是一个数组+链表的结构,数组的下标在HashMap中称为Bucket(水桶)值,每个数组项对应的是一个List(单向链表)
2.每个List中存放的是Entry对象,这个Entry对象包含键和值以及下一个Entry的地址。(其实就是数组中存放的是Entry对象,而Entry对象实际上是一个单向链表)。
存储过程:
1.通过key的hashCode()函数计算key的hash值,再通过hash值得出Bucket值(如何通过hash值的出Bucket值,哈希值 % 数组容量 = bucketIndex )
2.得出Bucket值后,如果该Bucket值对应的list是一个空列表,那么将生成entry对象,插入到list中,做为list的第一个元素
3.如果该Bucket值对应的list中已经有其它对象了(如果两个key的hash值一样就会发生),这个时候就发生了碰撞。
4.发生了碰撞后,新插入的key就会和链表中的其它entry的key进行比较,比较过程需要用到equals()方法
如果两个key的hashcode一样,并且equals方法比较也相同,那么HashMap就判断这两个key完全一样
5.如果两个key比较结果一样,由于HashMap不允许同时存在两个相同的key,那么新插入的entry就会覆盖旧entry
6.如果比较不一样,那么将新的entry插入到list的末尾
那么在这里,Entry<K,V>是Map的内部接口,HashMap中的内部类Node<K,V>(JDK1.8以前是Entry)实现了Map.Entry,HashMap内部通过Node数组存储数据。所以若要遍历HashMap,可获取entrySet再遍历。
transient Node[] table;
HashMap为何如此设计
HashMap中的数据结构是数组+单链表的组合,我们希望的是元素存放的更均匀,最理想的效果是,Entry数组中每个位置都只有一个元素,这样,查询的时候效率最高,不需要遍历单链表,也不需要通过equals去比较Key,而且空间利用率最大。那么怎么才能将分布最大的均匀化呢?那就是取余运算%,哈希值 % 数组容量 = bucketIndex
JDK1.8的修改
在JDK1.6中,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里。但是当位于一个链表中的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用数组+链表+红黑树来实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。