Map是开发中经常用到的一种键-值对的存储结构,在java中有几种常用实现。
HashMap
HashMap允许值和value为null,线程不安全。
创建的时候通常用默认的构造方法,还有其他的构造方法可以设置初始大小和负载因子,不设置的话默认初始大小是16,设置的话要求最小是2的4次幂16,最大是2的30次幂,如果设置的值不是2的N次幂,会往上取离它最近的2的N次幂,负载因子是0.75f,当容量大于容器大小x负载因子就会扩容。
HashMap内部是通过Node<K,V>(也有可能是TreeNode,TreeNode是Node子类)数组来存储数据的,Node是一个链表,Node里有个Node类型的next变量。
向HashMap添加KV的时候,通过Key的hash%size,这样通过数组就能直接找到位置,如果该位置为null,则通过KV创建一个Node,放在数组的对应位置,如果该位置已经有个Node了,判断该Node是不是TreeNode,如果是,则按照红黑树的方式添加,否则按照Node方式添加,添加的时候会遍历链表,判断添加的key和当前Node的key是否相同或者equals为true,如果是,替换value,如果没有,则添加个新的Node。
这里开始是用Node链表实现的,当链表长度大于等于8,将链表转换成红黑树,因为链表过长,在get查询的时候耗时过长。
在计算位置的时候,先要计算hash值,hash是通过hashcode经过扰动函数计算出来的,hashcode是32位的二进制数,将自身右移16位与原值进行异或运算.
(h = key.hashCode()) ^ (h >>> 16)
来加大低位的随机性,并且混合后的低位也掺杂了部分高位的信息。
这是Java8的算法,Java7的算法要复杂些。
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
其实也是高低位混合。
数组的索引是通过hash%size计算的,用hash值对数组长度求模,可以保证该值在0和数组长度-1之间。
hashmap要求大小是2的N次幂,因为当size是2的N次幂时,hash%size=hash&(zise-1),可以用与运算代替模运算,提高计算速度。
因为2的N次幂的二进制都是1+n个0,乘以2就是1左移1位,这里假设有一个hashcode值,假设当前size是32
hashcode = 1111 1111 1111 1111 1100 1001 1110 1000
size = 10 0000
可以发现hashcode对应size的1左边的值肯定是可以被size除尽的,那么hashcode%size的结果其实就是hashcode最左边的5位,所以和hashcode&(size-1)是相等。
HashTable
HashTable的key和value都不允许为null,初始容量为11,扩容时,以原容量2倍+1进行扩容,因为其容量不是2的N次幂,所以只能用模运算计算位置,这比hashmap要慢,HastTable是线程安全的,每个方法都有synchronized修饰。相对于HashMap来说HashTable的唯一优势就是线程安全,但是性能并不高,如果需要线程同步,可以使用ConcurrentHashMap.
ConcurrentHashMap
ConcurrentHashMap实现了ConcurrentMap接口,ConcurrentMap又实现了Map接口,所以ConcurrentHashMap也是Map的一种实现。它的大小和扩容机制都和HashMap一样,存取也差不多,区别就是put的时候为了线程安全会加锁,但是不是像HashTable那样整体加锁,而是对key对应的node加锁,这样如果两个线程同时put,如果根据key计算的位置不同是完全不影响,只有两个key经过hash扰动并求模后的值仍然一样时才会等待。