HashMap是一个哈希表结构,以键值对形式存取数据,允许为null值,key重复则覆盖,非同步,线程不安全,而且不保证顺序,如要保证顺序可使用LinkedHashMap,要保证安全可使用ConcurrentHashMap
1.默认容量
HashMap默认容量为16,但是是一个懒加载的机制,第一次put时才会申请内存空间,负载因子为0.75,初始化时也可自己设定默认容量,但是必须是2的次幂,不然会被自动调成2的次幂;
坑: 准备用HashMap存1w条数据,构造时传10000还会触发扩容吗?
不会扩容,因为如果构造时传1W,1W不是2的次幂,会自动转为2的14次幂,即16384,乘以负载因子0.75=12288,存入1W条数据绰绰有余。
为什么必须是2的n次方
如果用取模运算会让数据分布比较均匀,但是消耗比较大,所以hashMap是用hash&(n - 1) 来计算下标的,其中n是Map数组的长度,而在length是2的n次幂时hash&(n - 1)等价于hash%length,效率会更高 只有它的长度是2的n次方的时候,我们对它进行-1操作才能拿到二进制位数全部是1的值,这样对他进行按位与(&)才能够非常快速的用位运算的方式拿到数组的下标,并且分布还是均匀的。
2. 计算hash值的方法
ini
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
向右移16位且按位异或的操作,为了降低哈希碰撞的几率
JDK1.7 的时候是一堆异或操作和右移操作
3. JDK1.7 与 1.8 的区别
(1)JDK1.7采用头插法,JDK 1.8 采用尾插法。而改用尾插法的原因本人理解有两点:一是头插法会造成循环链表,二是需要在链表长度大于 8 时进行树化判断,也不能把链表的长度存在 map 中,那样会增加开支,所以需要遍历一遍来判断链表的长度是否达到 8 。
头插法就是插入新的节点,是从头部插入,原本的都往后排,createEntry方法
less
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash,key,value,e) //新节点的下一个节点等于原本的第一个节点
size++
所以多线程情况下容易变成环形链表,会导致查询一个不存在key,最终导CPU100%
原因为在JDK1.7的hashMap的resize()方法里有个transfer()方法
ini
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
// 这行也是重点
Entry<K,V> next = e.next; --------------(1)
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//计算出重新存放的位置
int i = indexFor(e.hash, newCapacity);
// 重点这三行
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
这个过程为,本来有链表 1------>2,然后线程一和线程二同时走到transfe方法的while循环里的(1)处, 此e = 1,e.next=2,然后线程二挂起,线程一把循环执行完之后并把线程一生成的新table放在内存里,此时链表为2------>1,接着线程二执行,但是现在线程二还是e = 1,e.next=2,然后执行重点三行代码会出现1.next=2,就变成了1------>2------>1,循环链表就形成了
还有一种情况:通过精心构造的而已请求引发Dos,比如可以通过精心构造的http请求,就是一些hash值相同的key,让tomcat内部产生大量的链表,消耗大量CPU发生Dos(拒绝服务攻击)
(2)出现哈希冲突时,1.7把数据存放在链表,1.8是先放在链表,链表长度达到8并且数组的长度不小于64时 就转成红黑树,低于6重新转换为链表
为什么超过8会变成红黑树?
红黑树占用空间是链表的两倍,空间损耗比较大,所以没有直接使用红黑树,源码里说是符合泊松分布,哈希冲突八次的几率较小,转为红黑树是因为要保证在冲突比较多的时候的查询效率
(3) 1.7的addEntry()方法里的扩容条件是map的元素大于阈值且存在哈希冲突,1.8扩容条件是
HashMap中的是元素个数大于阈值 或 数组中的链表长度达到8且Node数组长度小于64时
(4)1.7添加新元素是先扩容并重新计算下标,再进行插入,1.8后是先进行插入,如果 ++size > threshold再按照扩容后的规律统一计算。扩容后的统一规律:
(5)new HashMap()之后并不会在堆区申请内存空间,因为构造方法里没有 new 那个数组 ,JDK1.7中 ,put 方法中的 inflateTable() 方法里如果是 EMPTY_TABLE 时才会进行 new Entry,而1.8中是在 put() 方法里如果数组的长度是0的时候调用 resize() 方法,在 resize() 中进行 new Node[] 的。
4. 树化的条件
链表长度达到8的时候执行treeifyBin() 方法,但还会进行判断,tab.length >= 64,的时候才会树化,不然只会进行扩容
遍历HashMap的方式
vbnet
public static void main(String[] args) {
HashMap<Integer,Integer> hashMap = new HashMap();
for (int i = 0; i < 10; i++) {
hashMap.put(i,i);
}
// Set set = hashMap.keySet();
// for (Object o : set) {
// System.out.println("Key: "+o+" Value: "+hashMap.get(o));
// }
Set<Map.Entry<Integer, Integer>> entries = hashMap.entrySet();
for(Map.Entry<Integer, Integer> entry : entries){
System.out.println("key: "+ entry.getKey() + "; value: " + entry.getValue());
}
}
LinkedHashMap怎么,保持有序的?
LinkedHashMap有一个head变量,就是头结点,tail,尾节点,还定义了一个Entry类,该类继承了HashMap.Node类,添加了一个before,after,就和linkedList相似,指向他的前一个Entry和后一个Entry
LinkedHashMap 重写了 HashMap 的 newNode() 方法,在new Node() 时,调用了 linkNodeLast()。
将新元素的 before 指向 tail 节点,并把 tail 节点变为最新的节点,实现了双向链表功能,保证有序。
LinkedHashMap 比 HashMap 多了一个 accessOrder ,accessOrder 为 true ,那么除了 new Node() 时会调整链表结构,新增修改和查看等操作都会通过 被重写的 afterNodeAccess() 方法修改链表结构。
TreeMap怎么保持有序?
TreeMap是基于红黑树实现的,保证二楼键的有序性,迭代时根据键的大小排序
红黑树性质
1.每个节点都有两个子节点,但是子节点可能为空
2.每个节点不是黑色就是红色,根节点一定是黑色
3.每个红色节点的两个子节点都是黑色,不可能有连在一起的红色节点
每次插入和删除的时候,如果打破红黑树的平衡,都会自动做旋转和颜色变化,以达到保持平衡,新插入的节点的位置是根据key的大小来判断的