总结:
以下都是个人的学习总结整理,如有错误,欢迎评论指出
一、HashMap类
实现了Map、Cloneable、Serializable接口,继承AbstractMap类
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
/**
* Map接口: 实现键值对,Map接口规定了一个key对应一个value
* HashMap使用该接口用来替换Dictionary类
*
* AbstractMap类: 继承Map的抽象类,减少Map操作的实现
*
* Cloneable接口: 可以显示的调用Object.clone()方法,合法的对该类
* 实例进行字段复制
*
* Serializable接口: 实现该接口后,该类可以被序列化和反序列化
*/
二、HashMap是否线程安全?
HashMap不是线程安全的,在并发的场景下使用ConcurrentHashMap代替
三、HashMap的内部实现是什么?
内部实现:数组 + 链表 + 红黑树 (JDK 1.8之后才加上了红黑树)
数组特点:查找快,时间复杂度为O(1);删除慢,时间复杂度为O(n)
链表特点:查找慢,时间复杂度为O(n); 插入/删除快,时间复杂度为O(1)
ArrayList 使用的就是数组
LinkedList 使用的是链表,而且LinkedList是一个双向链表
四、HashMap其余知识点
1. HashMap扩容后,索引位置变化
HashMap触发扩容后,阈值会变成原来的两倍,并且会对所有节点进行重Hash分布,重Hash分布后节点的新位置只可能在两个:"原索引的位置“ 或者 “原索引+原容量的位置”
(例:HashMap默认容量为16,默认负载因子为0.75,所以默认阈值为12;扩容后阈值会变成24,容量为32)
(例:原容量capacity为16,扩容前,索引位置是5的节点,扩容后,只可能分布在新表的"索引位置5"或者"索引位置21(5+16)")
2. 导致HashMap扩容后,同一索引位置的节点重Hash最多分布在两个位置的原因
(1) 哈希表的长度始终是2的幂次方
(2) 索引位置的计算方法是 (table.length - 1) & h ( 哈希值 与 表长度-1 的 位与运算)
3. 如何优化HashMap?
由于HashMap扩容是一个比较耗时的操作,定义HashMap时指定初始容量,即指定HashMap的大小
4. JDK8为什么要把链表改成红黑树?
链表查找的时间复杂度为O(n),如果链表越长,查询越慢;而红黑树各种操作的时间复杂度为O(logn)
(红黑树查询效率远大于链表,但是插入/删除操作却比链表要慢)
5. JDK 1.8 中 链表 转 红黑树
当同一个索引位置的节点在增加后达到9个时,若此时数组的长度大于等于64时,则会将链表节点(Node)转红黑树节点(TreeNode),转为红黑树节点后,链表结构其实还是存在的,通过next属性维持。如若节点个数达到9个,但是数组长度不足64时,则不会触发链表转红黑树,而是只进行数组扩容
6. JDK 1.8 中 红黑树 转 链表
当同一索引位置的节点在移除后只有6个时,而且该节点为红黑树节点时,则会触发红黑树转链表
7. JDK 1.8 之前HashMap存在死循环问题
造成原因:由于数组扩容后,同一索引位置的节点顺序会反掉
8. HashMap 与 HashTable 的区别
HashMap | HashTable |
---|---|
允许 key 或者 value 为 null | 不允许 key 或者 value 为 null |
初始容量为 16 | 初始容量为 11 |
扩容为原来的 2 倍 | 扩容为原来的 2 倍加 1 |
线程不安全 | 线程安全 |
hash值是重新计算的 | 直接使用hashCode |
采用异步处理方式,性能更高 | 采用同步处理方式,性能相对较低 |
9. 重写equals()方法后,是否一定要重写hashCode()方法?为什么?
重写equals()方法后,需要重写hashCode()方法。
hashCode规定:
两个对象相等(即equals()返回true),hashCode一定相同;两个对象hashCode相同,两个对象不一定相等;
重写equals,而不重写hashCode方法,默认调动Object类的hashCode()方法,会导致两个对象equals结果相同,但是hashCode()不同。简言之,重写equals方法后,重写hashCode方法就是为了确保比较的两个对象是同一对象