对比Hashtable、 HashMap、 TreeMap有什么不同? 谈谈你对HashMap的掌握。
-
Hashtable、 HashMap、 TreeMap都是最常见的一些Map实现,是以键值对的形式存储和操作数据的容器类型。
-
Hashtable是早期Java类库提供的一个哈希表实现,本身是同步的,不支持null键和值,由于同步导致的性能开销,所以已经很少被推荐使用。
自我补充:遗留的类,不建议使用。 -
HashMap是应用更加广泛的哈希表实现,行为上大致上与HashTable一致,主要区别在于HashMap不是同步的,支持null键和值等。通常情况下, HashMap进行put或者get操
作,可以达到常数时间的性能,所以它是绝大部分利用键值对存取场景的首选,比如,实现一个用户ID和用户信息对应的运行时存储结构。 -
TreeMap则是基于红黑树的一种提供顺序访问的Map,和HashMap不同,它的get、 put、 remove之类操作都是O(log(n))的时间复杂度,具体顺序可以由指定的Comparator来决定,或者根据键的自然顺序来判断。
类结构图(不包括并发包)
HashMap分析
回顾散列(hash)数据结构
散列表用的是数组支持按照下标随机访问的数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。
由于数组下标随机访问时间复杂度是O(1)的,hash表的时间复杂度也是O(1)的。
数组的随机访问特性:
我们知道数组创建是一段连续的内存空间,并且存储相同的数据类型。
如上图,我们存储整型数据数组{500,562,14}.如果知道第0个数组的内存地址,我们就知道1这个下标的内存地址。因为整型数组占用4个字节。
利用这个特性。我们只要知道数组下标,也就是数据在数组中的位置,就可以知道内存地址位置。从而快速访问,时间复杂度O(1)。
但是,如果不是数组下标,只是知道数据的值,那么必须整体遍历一遍复杂度O(n)。
为什么数组O(1),Hash O(1)
举个例子,如果校园运动会89名选手参加,胸前贴号码牌。这个号码牌就是1-89.
如果将这89名选手放到数组里面,标号为1的选手,放到1的数组下标中,2就放2的下标这样数组下标和标号一一对应了,我们查找的时间复杂度就是O(1)。
当然实际情况hash不会这么简单。
就像现实情况下,参赛的编号可能不是1-89,而是3年二班第二位选手就是我们之前的2号,我们可以表示成030202 -->2 号选手–>数组下标2。我们只需要截取后两位,这样我们还是能找到数组下标2的位置。
我们抽象出来就是一个(key,value)形式。 (“1号选手”,“张三”)
1号选手叫:key 键
张三叫:value值
key的后两位的值叫做:散列值,hash值,hash code
选手编号转化为数组下标的映射方法hash(key),叫做散列函数(Hash函数)。
顺便提一下hash code
key的后两位的值叫做hash code。所以我们需要注意的是,散列值不是内存地址,就是代表对象中hash表的位置,是一个整数。
从整体来看key和table的关系:
java中String的 hash code如何实现的?:
《Effective Java》中提到Hashcode中的约定:
- 应用程序运行期间:只要equals方法操作,用到的信息没有被修改,同一对象调用多次,hashCode返回同一个整数。
同一程序调用多次:每次返回的整数可以不一致。 - equals相等, hashCode一定要相等。
- equals不等,hash code不一定不同,但是程序员应该知道,给不想打呢个的对象产生截然不同的整数结果,有可能提高散列表(hash table)性能。
所以根据第三条,我们要是设计一个散列函数,散列函数生成的值要尽可能随机并且均匀分布。
并且给出了一些办法。详细请参考:
《Effective Java》
第二版第三章第9条:覆盖equals是总要覆盖hash code.
散列函数工业级别:
工业级别不能像我们描述的那么粗糙,需要满足三个条件:
1.散列非负整数
2.key1 = key2 ,hash(key1) == hash(key2),相同的key的到的散列值也是相同的。
3.key1 !=key2 ,hash(key1) != key2 散列冲突问题。
工业级别的hash算法有:MD5,SHA,CRC。但是工业级别也无法完美解决散列冲突。
3不好理解:如果存储键值对(x,“123”) 通过hash函数hash(x)得到"123"的存储位置。
如果再来一个(y,“456”),hash函数hash(y), 得到的hash值是一样的,这两个对象的存储地址就冲突了。我们叫做散列冲突问题。
如何解决散列冲突?
解决散列冲突主要有几种:
开放地址法:如果出现散列冲突,我们就重新探测一个空闲的位置,将其插入。
我们可以使用线性探测来插入位置。
橘黄色表示有值,黄色表示没有值。
插入:散列表的大小为10,如果x hash之后,散列到下标7,但是7是橘黄色的有值。所以需要顺序往后找,如果没找到,再次从开始找。
最终找到2的位置。
查找:还是依次查找。
二次探测(Quadratic probing):和线性探测很像。 线性探测每次探测的步长是1,那它探测的下标序列就是hash(key)+0,hash(key)+1,hash(key)+2……而二次探测探测的步长就变成了原来的“二次方”,也就是说,它探测的下标序列就是hash(key)+0,hash(key)+12,hash(key)+22.
双重散列(Double hashing):散列两次。如果散列之后被占用,再次散列。
HashMap中如何解决散列冲突?
综合考虑了所有因素,采用链地址法。即,数组(hash)+链表
在散列表中,每个buket或者slot对应一条链表,散列值相同的元素我们放到相同槽位对应的链表中。
顺便提一句,jdk1.8中还添加了红黑树,防止链表过长。
HashMap 的重要属性:
(1)Node[] table:
transient Node<K,V>[] table;
结构:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //用来定位数组索引位置
final K key;
V value;
Node