HashMap继承了AbstractMap这个抽象类 并且实现了Map这个接口,可以实现clone和序列化
底层数据结构 : 数组 + 单链表 + 红黑树
【说明】 每一个数组+ 单链表/红黑树 叫做桶 也叫做段
定义了hash表所对应的数组的长度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
hash表中数组的最大长度
static final int MAXIMUM_CAPACITY = 1 << 30;
扩充因子,数组里存放的值达到数组长度的75%,就开始扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
单链表的长度,如果超过8,就把单链表转为红黑树
final int TREEIFY_THRESHOLD = 8;
桶中红黑树立元素个数,小等于6时,转为单链表
static final int UNTREEIFY_THRESHOLD = 6;
存放点链表或树结构的首地址(不存具体数据)
HashMap在JDK1.8及以后的版本中引⼊了红⿊树结构,若桶中链表元素个数⼤于等于8时,链表转换成树结构;若桶中链表元素个数⼩于等于6时,树结构还原成链表。因为红⿊树的平均查找⻓度是log(n),⻓度为8的时候,平均查找⻓度为3,如果继续使链表,平均查找⻓度为8/2=4,这才有转换为树的必要。链表⻓度如果是⼩于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和⽣成树的时间并不会太短。还有选择6和8,中间有个差值7可以有效防⽌链表和树频繁转换。假设⼀下,如果设计成链表个数超过8则链表转换成树结构,链表个数⼩于8则树结构转换成链表,如果⼀个HashMap不停的插⼊、删除元素,链表个数在8左右徘徊,就会频繁的发⽣树转链表、链表转树,效率会很低。
也就是说如果一开始为6此时是链表 那么再插入一个元素变为7的时候依旧是链表 再增加一个 变为8的时候才转为红黑树
但是如果 一开始为8此时为红黑树 那么删除一个元素变7的时候依旧是红黑树 再删除一个 变为6的时候才转换为链表
因为在HashMap源码中 有一个静态内部类Node
无序、key值唯一,如果Key重复,value值会进行覆盖
在存值的时候,首先对Key进行hash值计算,计算的结果就是hash表中数组的存放的位置
hashMap的key允许存null值,具体来说 hashMap的key和value都可以存null值
先定义变量h用来接收hashCode值
当key为null的时候就将元素存储在数组中0这个位置
首先计算出key的hashcode值
将hashcode的值 无符号 向右移16位做异或运算,此时得到hash值
再将hash值与数组长度-1去做与运算
此时得出来的这个结果是一个0~数组长度中间的一个数,这个数就是数组中的索引,也就是地址在数组中存放的位置
【异或运算】相同为0不同为1
【与运算】 0&0=0;0&1=0;1&0=0;1&1=1
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
如果当前数组的存放位置为null 证明没有存放元素 那么就new Node(key,value) 将new Node之后的地址存放在数组的元素里 将key 和 value的值存放在链表中
如果当前位置有元素(hash值发生冲突 不同key经过hash计算结果可能是一样的),就顺着单链表,从首元素开始,逐个使用equals比 较key值是否相等
如果key的equals值都不相等,那么new Node节点,用来存放key和value的值,把这个节点所对应的地址放在单链表的末尾, 存放后,判断单链表的长度是否大于 8,如果是,把单链表转为红黑树,不是的话依旧是链表
如果通过equals比较完,当前key和某个链表中Node的key相等(hash冲突),则使用当前的value去覆盖掉原有的value
扩容2倍后,计算出的hash值锁产生的hash冲突的几率最小。
扩容后,由于数组长度发生了改变,所有元素都要重新计算hash值(与数组长度-1做与运算),存放位置可能会发生改变
如果存放元素超过12个,最后new HashMap的时候指定数组的长度 (用存放的个数除以12)
因为扩容所有元素都要重新计算hash值所以我们应该尽量减少数组的扩容,根据存放的元素个数在最开始的时候就定义好数组的长度,具体执行的方法如下
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
1. Hashtable是线程安全,而HashMap则非线程安全,Hashtable的实现方法里面大部分都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合。
2. HashMap的键和值都可以为null,而Hashtable的键值都不能为null。
3. HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。HashMap扩展容量是当前容量翻倍即:capacity*2,Hashtable扩展容量是容量翻倍+1即:capacity*2+1(关于扩容和填充因子后面会讲)
4. 两者的哈希算法不同,HashMap是先对key(键)求hashCode码,然后再把这个码值得高位和低位做异或运算,源码如下:
HashMap: 得到key值得hashcode,
对hashcode值异或运算
对异或计算的结果 和 初始容量(数组大小)-1做&运算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
i = (n - 1) & hash
然后把hash(key)返回的哈希值与HashMap的初始容量(也叫初始数组的长度)减一做&(与运算)就可以计算出此键值对应该保存到数组的那个位置上(hash&(n-1))。这里为什么不用key本身的hashcode方法,而又是右移动16位又是异或操作。开发人员这样做的目的是什么呢?是当数组容量很小的时候,计算元素在数组中的位置(n-1)&hash,只用到了hash值的低位,这样当不同的hash值低位相同,高位不同的时候会产生冲突。实际上的hash值将hashcode低16位与高16位做异或运算,相当于混合了高位和低位,增加了随机性。当然是冲突越少越好,元素的分布越随机越好。
而Hashtable计算位置的方式如下:
int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;
直接计算key的哈希码,然后与2的31次方做&(与运算),然后对数组长度取余数计算位置。