本文目录结构
键值对Map家族说明
Java为数据结构中的键值对定义了一个接口java.util.Map,此接口主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap和TreeMap,类继承关系如下图所示:
下面针对各个实现类的特点做一些说明:
- HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
- Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
- LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
- TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
对于上述四种Map类型的类,要求映射中的key是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。
HashMap存储结构详解
首先来看JDK8中HashMap存储结构示例代码
/** Jdk8 HashMap 分解详解 */
public class HashMapAnalysis<K,V> {
/** Node实现了Map.Entry<K,V>用于数据存储键值对,如果K的hashCode相同,存储在Node实例中链表结构next当中,后出现的相同hashCode的键值对会出现在链表的前面。如果Key的相同(equals()方法),会替换相同hashCode相同Key的值 */
transient Node<K,V>[] table;
/** 当前Map的键值对的数量 */
transient int size;
/** Map需要扩容的负载因子,默认0.75 */
final float loadFactor = 0.75f ;
/** 下一个元素大小,如果达到扩容数字(容量*加载因子),需要resize(双倍扩容) */
int threshold;
/** HashMap中存储对象Node对象结构 */
private class Node<K, V> implements Map.Entry<K,V>{
/** 当前Key的hashCode值 */
final int hash;
/** 当前Entry中Key对象 */
final K key;
/** 当前Entry中Value对象 */
V value;
/** 当前Entry中Key的hashCode一样Entry的下一个Entry(链表存储) */
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash; this.key = key; this.value = value; this.next = next;
}
}
}
看属性元素的说明,就能知道意思了,也可以使用下图来理解:
HashMap使用table数组+链表存储Node<K,V>的,但是如何将键值对Key的hashCode值对应到table数组下标呢,看如下源码:
//获得Key的扰动hashcode值,称为“扰动函数”
static final int hash(Object key) {
int h;
/** 1:>>>无符号右移16位,等于取hashCode的前16位,高位补0(hashCode值为int类型,占4个直接)
2:^位异或运算(相同为0、不同为1)
目的:此函数一般称为“扰动函数”,将hash值的高16位与低16位进行异或操作,
混合了高位信息和低位信息,使得得到的新值更具有随机性,
可以规避hashCode低位上有许多冲突造成HashMap出现较大的桶碰撞几率。
*/
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//取模jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的(如下putVal()方法第4行)
static int indexFor(int h, int length) {
/** length为当前table桶数组的大小(都是2个次方大小),
length-1就是数组长度减一相当于一个低位掩码(低位全是1,以数组初始长度16为例,16-1=15,二进制表示就是00000000 00000000 00000000 00001111),
length-1与hashcode值做位与&运算,就相当于取模运算,刚好获得数组长度范围内的下标,示例如下
00000000 00000000 00000000 00001111
& 11101010 00101000 10010101 10101110
-------------------------------------------------------
00000000 00000000 00000000 00001110
*/
return h & (length-1);
}
//HashMap的put()方法源码,包含取模操作的jdk1.8源码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//上面的n就是当前table[]桶数组的大小
//下面就是n-1与原hash值取模的结果,作为table[]桶数组的下标值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
/** 这里的(length-1)^hash刚好论证了,为什么HashMap扩容大小都是2的幂次方,方面低位掩码取模操作 */
HashMap非线程安全导致死循环问题说明
在多线程工作环境下,HashMap容量达到临界值的put操作后,其他线程get操作容易导致死循环。是因为HashMap容量达到临界值会进行2倍扩容,扩容完成之后会进行数据转移的工作在transfer(JDK1.8中已没有此方法)方法中,单线程时相同hashCode的Entry链表数据转移完成后元素会倒序,如果此时发生多线程操作,就容易造成死循环,死循环原理详解本文不详细说明(内容较多可另起文章说明)。ConcurrentHashMap将原本HashMap的单个数组存储形式,分解成多个段(Segment)的形态,每个段里面的结构和以前HashMap结构类似,这样使用分段锁,来减少锁的范围,提高并发效率。
HashMap常见面试题
HashTable, HashMap,TreeMap区别?
- HashTable线程同步,HashMap非线程同步
- HashTable不允许<键,值>有空值,HashMap允许<键,值>有空值
- HashTable使用Enumeration,HashMap使用Iterator
- HashTable中hash数组的默认大小是11,增加方式的old*2+1,HashMap中hash数组的默认大小是16,增长方式一定是2的指数倍。
- TreeMap能够把它保存的记录根据键排序,默认是按升序排序
HashMap是不是有序的连环炮式发问
【你肯定回答说,不是有序的】。那面试官就会继续问你,有没有有顺序的Map实现类? 【你说有TreeMap和LinkedHashMap】。 那么面试官接下来就可能会问你,TreeMap和LinkedHashMap是如何保证它的顺序的?【LinkedHashMap根据put的顺序保证有序,TreeMap根据Key元素的Comparable接口实现方法保证有序】。 如果你依然回答上来了,那么面试官还会继续问你,你觉得它们两个哪个的有序实现比较好? 如果你依然可以回答的话,那么面试官会继续问你,你觉得还有没有比它更好或者更高效的实现方式? 如果你还能说出来的话,那么就你所说的实现方式肯定依然可以问你很多问题。 以上就是一个面试官一步一步提问的例子。所以,如果你了解的不多,千万不要敷衍,因为可能下一个问题你就暴露了,还不如直接说不会,把这个问题结束掉,赶紧切换到你熟悉的领域。