Map
Map相关的内容在面试过程中都是一个重要的点。问深了会涉及到很多数据结构和线程相关的问题。
- 你了解Map吗?常用的Map有哪些?
Map是定义了适合存储“键值对”元素的接口
常见的Map实现类有HashMap、Hashtable、LinkedHashMap、TreeMap、ConcurrentHashMap - HashMap的底层原理
HashMap底层使用的数据结构是哈希表(又叫散列表)。哈希表又由数组+链表实现。数组的特点是空间连续,查询快,而增删慢;链表的特点是空间不连续,通过指针寻址,所以查询慢,而增删快。
哈希表的核心思想就是让关键码值和存储位置建立一一映射关系,以通过Key直接快速查找到相应的Value。用来建立一一映射关系过程的函数,就是哈希函数。
提到哈希函数就不可避免的扯到它的一些实现方式(如:直接寻址法、数字分析法、平方取中法、折叠法、随机数法、除留余数法等),以及在遇到哈希冲突后的解决方法(开放地址法、再哈希法、链地址法、公共溢出区法)。
HashMap实际采用的是位运算+链地址法解决哈希地址冲突的方案。
位运算计算hash值,如下:
/**
* 下面是jdk1.8版本中的hash算法,相比jdk1.7中少了三次位运算
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 经过hash()后的int值比较大,不适合我们数组。
* 使用h&(len-1)操作,返回的索引值比较小适合我们的数组
* 位运算法相比于除留余数法的模运算效率更高
* jdk1.7版本中还有此静态方法;1.8中取消了此方法,在使用此方法处直接使用链式编程,实质不变
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
用下面一幅图描绘下此次hash的过程,用心感受一下
链地址法解决hash冲突,如下:
当两个Key的hash值一致时,就需要像table数组中的Entry对象(即:链表)中追加或替换要put的节点。
jdk1.7和jdk1.8对于此处的实现还是有些异同的。本处只讨论基本原理,不对源码在一段一段的详细分析。若有兴趣可从两个版本中的put()方法可对比看出。
jdk1.7: 若需要新增节点,则向链表头部新增节点,新节点的next域指向原头部节点。
jdk1.8: 若需要新增节点,则向链表尾部新增节点,原尾部节点next域指向新节点。重点来了,jdk8中在HashMap中就开始引入红黑树的数据结构,一旦链表中的节点个数超过了TREEIFY_THRESHOLD - 1这个阈值,就将链表转换成红黑树的结构
- HashMap什么时候扩容(resize、rehash),如何扩容
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 数组容量:默认16
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 负载因子:默认0.75
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量
jdk1.7中的触发条件:
/**
* jdk1.7中对threshold定义如下:
* threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
*/
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
jdk1.8中的触发条件:
/**
* jdk1.8中对threshold定义如下:
* threshold = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)
*/
if (++size > threshold)
resize();
由此可看出触发resize的条件均是map中存放的元素数量size值大于threshold值。差异点在threshold和resize()的具体实现。
1)对于threshold其实差异并不大,在元素数量不达到MAXIMUM_CAPACITY这个量级之前均可认作threshold=负载因子容量值。所以初始化后map的容量一旦超过0.7516=12,就会resize()。
2)由于对于jdk1.8中对HashMap的数据结构进行了优化,在超过一定阈值后会将链表优化成红黑树,在此不做详细分析。对于jdk1.7中,便可通过代码直观的看出,将容量扩充为当前table长度的2倍。
由于扩容后数组容量增加,还需要对原有数据重新分配索引,是一个比较耗性能的操作。若事先能估算出Map中预要存放的元素数量,建议初始化时就设置其容量,规则为:初始化容量*装载因子>预估元素数量,初始化容量建议取2的幂次方。
- HashMap、Hashtable的比较
在jdk1.8版本以前HashMap和Hashtable采用相同的存储结构、实现基本类似。不同的是:
(1)HashMap的key和value是允许为空的,key为null的节点放在table[0]中;而Hashtable则都不允许。
(2)HashMap是非线程安全的;HashTable中的方法都加synchronized进行同步,是线程安全的。正因此,在线程安全环境下,HashMap的效率比HashTable的效率高。
(3)HashMap的初始化数组大小为16,阈值为160.75=12,每次扩容都将数组容量扩大2倍;
Hashtable的初始化数组大小为11,阈值为(int)110.75=8,每次扩容执行int newCapacity = (oldCapacity << 1) + 1,即乘2加1;
(4)HashMap中的hash值是取得key的hashcode后进行了一次位运算,同时计算索引时使用位运算;
而Hashtable则直接使用key的hashcode作为hash值,在计算索引时,用取模运算。
// 上面已经分析过HashMap的hash和index方式,不在赘述
// 下面代码是Hashtable中实现hash和index的方式
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
(5) 两个遍历方式的内部实现上不同。HashMap,Hashtable都使用了 Iterator。但由于历史原 因,Hashtable还使用了Enumeration的方式 。
5. TreeMap的特点(未完待续)