1:map集合简述:
2:HashMap集合的实现:
3:HashMap的成员变量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
-> 数组默认初始容量:16
static final int MAXIMUM_CAPACITY = 1 << 30;
-> 数组最大容量2 ^ 30 次方
static final float DEFAULT_LOAD_FACTOR = 0.75f;
-> 默认负载因子的大小:0.75
static final int MIN_TREEIFY_CAPACITY = 64;
-> 树形最小容量:哈希表的最小树形化容量,超过此值允许表中桶转化成红黑树
static final int TREEIFY_THRESHOLD = 8;
-> 树形阈值:当链表长度达到8时,将链表转化为红黑树
static final int UNTREEIFY_THRESHOLD = 6;
-> 树形阈值:当链表长度小于6时,将红黑树转化为链表
transient int modCount; -> hashmap修改次数
int threshold; -> 可存储key-value 键值对的临界值 需要扩充时;值 = 容量 * 加载因子
transient int size; 已存储key-value 键值对数量
final float loadFactor; -> 负载因子
transient Set< Map.Entry< K,V >> entrySet; -> 缓存的键值对集合
transient Node< K,V>[] table; -> 链表数组(用于存储hashmap的数据)
4:重写hashCode&&equals方法
如果没有重写 hashcode 方法,JDK 默认使用 Object 类 native 的 hashCode 方法,返回的是一般是一个与存储地址相关联的数
HashMap不能存储key值相同的数据,是因为在存储时,会先判断hashCode是否相同,紧接着equals继续判断
所以用到hashCode时,必然要重写hashCode和equals方法
例:写一个Student类,重写hashCode和equals方法
public class Student { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return this.name+"--"+this.age; } @Override public boolean equals(Object o) { Student student=(Student) o; return this.name.equals(student.name)&&this.age==student.age; } @Override public int hashCode() { return name.hashCode()+age*38; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
5:resize机制
HashMap的扩容机制就是重新申请一个容量是当前的2倍的桶数组,然后将原先的记录逐个重新映射到新的桶里面,然后将原先的桶逐个置为null使得引用失效。
扩容处理会遍历所有的元素,时间复杂度很高;经过一次扩容处理后,元素会更加均匀的分布在各个桶中,会提升访问效率。所以我们尽量避免进行扩容处理,当我们知道需要存储数据的个数时,在newHashMap时就给定初始容量,避免重复扩容
给定扩容容量我们必须要给定容量大于我们预计数据量的 1.34 倍,并且为2的幂次方
例如:如果是2个数据的话,将初始化容量设置为4。
如果预计大概会插入 12 条数据的话,那么初始容量为16简直是完美,一点不浪费,而且也不会扩容。
6:为什么HashMap线程不安全
1>put的时候导致的多线程数据不一致。
这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
2>发生在扩容时,重新调整HashMap大小的时候,多个线程确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在LinkedList中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在LinkedList的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。这个时候,你可以质问面试官,为什么这么奇怪,要在多线程的环境下使用HashMap呢?