Map接口
1.HashMap与Hashtable、HashSet、HashSet的区别
HashMap与Hashtable的区别
HashMap | Hashtable | |
---|---|---|
线程是否安全 | 非线程安全 | 线程安全(内部方法基本都经过synchronized修饰) |
效率 | 效率较高 | 效率较低 |
是否支持空键值 | 支持(null作为key只能有一个、作为value可以有多个) | 不支持(会抛出NullPointerException) |
初始容量大小 | 16(默认初始大小) 将其扩充为2的幂次方(指定了容量) | 11(默认初始大小) 使用指定的容量(指定了容量) |
扩充容量大小 | 每次扩容的容量为原来的两倍 | 每次扩容的容量为原来的2n+1倍 |
底层数据结构 | 数组+链表+红黑树(当链表长度超过8时,链表转换为红黑树。) | 数组+链表 |
HashMap与Hashtable的区别
HashMap | HashSet |
---|---|
实现了Map接口 | 实现Set接口 |
存储键值对 | 进存储对象 |
调用put()向map中添加元素 | 调用add()方法向Set中添加元素 |
HashMap使用键(key)计算hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性 |
HashMap和TreeMap的区别
HashMap和TreMap都继承自AbstractMap,但是需要注意的是TreeMap还实现了NavigableMap接口和sortedMap接口
实现NavigableMap接口让TreeMap有了对集合内元素搜索的能力
注意:相比于HashMap来说TreeMap主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力.
2.HashMap的底层实现
JDK1.8之前
JDK1.8之前HashMap底层是 数组+链表 结合在一起使用也就是链表散列。HashMap通过key的hashcode经过扰动函数 处理过后得到hash值,然后通过(n-1) & hash 判断当前元素存放的位置(这里n指的是数组的长度),如果当前位置存在元素的话,就判断该元素于要存入的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同的话就通过拉链法(将链表和数组相结合。也就是说创建⼀个链表数组,数组中每⼀格就是⼀个链表。若遇到哈希冲突,则将冲突的值加到链表中即可 )"解决冲突)
关于这个扰动函数是什么:指的就是HashMap的hash方法。使用hash方法就是函数为了放置一些实现比较差的hashCode()方法,换句话说使用这个函数之后可以减少冲突。
对比JDK1.8和JDK1.7的源码来说明如何扰动
JDK1.8HashMap的hash方法源码:
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// e>fÅ⽆符号右移,忽略符号位,空位都以0补⻬
return (key }~ null) ? 0 : (h = key.hashCode()) ^ (h e>f 16);
}
对比一下JDK1.7的HashMap的hash方法源码:
static int hash(int h) {
// 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 e>f 20) ^ (h e>f 12);
return h ^ (h e>f 7) ^ (h e>f 4);
}
相比于JDK1.8的hash方法,JDK1.7的hash方法的性能会稍微差一点,毕竟只扰动了4次
JDK1.8之后
相⽐于之前的版本, JDK1.8 之后在解决哈希冲突时有了较⼤的变化,当链表⻓度⼤于阈值(默认为8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。(TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都⽤到了红⿊树。红⿊树就是为了解决⼆叉
查找树的缺陷,因为⼆叉查找树在某些情况下会退化成⼀个线性结构)
3.HashMap的长度为什么是2的幂次方?
为了能让 HashMap 存取⾼效,尽量较少碰撞,也就是要尽量把数据分配均匀。
Hash 值的范围值-2147483648 到2147483647,前后加起来⼤概 40 亿的映射空间,只要哈希函数映射得⽐较均匀松散,⼀般应⽤是很难出现碰撞的。但问题是⼀个 40 亿⻓度的数组,内存是放不下的。所以这个散列值是不能直接拿来⽤的。⽤之前还要先做对数组的⻓度取模运算,得到的余数才能⽤来要存放的位置也就是对应的数组下标。这个数组下标的计算⽅法是“ (n - 1) & hash ”。(n 代表数组⻓度)。这也就解释了 HashMap 的⻓度为什么是 2 的幂次⽅。
这个算法应该如何设计呢?
我们⾸先可能会想到采⽤%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减⼀的与(&)操作(也就是说 hash%lengthFGhash&(length-1)的前提是 length 是 2的 n 次⽅;)。” 并且 采⽤⼆进制位操作 &,相对于%能够提⾼运算效率。