HashMap存储数据的方式:
jdk1.8之前,HashMap底层通过数组 + 链表,java8及之后通过数组 + 链表/红黑树 实现,通过计算key的哈希值,找到元素存储的位置,jdk1.7之后通过 ( n - 1) & hash 来计算元素在数组中的存储位置,并且hash值是经过一定扰动的,减少冲突。存入元素时,如果目标位置上已经有了元素,会判断key是否相等,相等就直接覆盖,不相等就向后插入,链表长度大于8就会将链表转为红黑树,提升查询性能,小于6就会再转为链表,降低维护成本。
负载因子:
默认为0.75,用于判断是否需要扩容,如果size > 负载因子 * Capacity ,就扩容为原来的两倍
负载因子过高,会导致hash冲突更频繁,导致性能下降
过低会导致内存空间的浪费
如果已知需要多大的空间(n),可以将HashMap容量初始化为 n / 负载因子 + 1, + 1 主要是为了防止向下取整,导致容量不够
hashCode 与equals方法
规范:如果两个对象,使用equals判断为true,那么他们的hashCode值必须相等
hashCode用于计算key的hash值,确定元素的存储位置
equals方法,用于判断两个key是否相同
为什么要重写hashCode和equals方法,
不重写equals方法的话,会使用从父继承过来的equals方法,没有显式的使用extends 的话,用的就是Object类的equals方法,也就是单纯的比较对象的地址是否相同,但一般要根据实际业务来判断是否相同
为什么要重写hashCode
前面说过hashCode使用来计算对象存储位置的,没有重写的话,默认是计算对象地址的hash值,这就可能导致,我们逻辑上认为相同的元素,被存在了不同的桶中
为什么HashMap扩容时使用2 的m次方倍?
当n为2的m次方倍时,(n - 1) & hash == hash % n,而%运算很慢,始终确保HashMap的Capacity 为2的m次方倍,可以用这个属性来提高计算速度,并且可以让元素分布更均匀 2^n - 1后的二进制表现形式,后n 位都是1,相比于有0的情况,元素分布更均匀
使用HashMap提升性能的技巧:
1. 前面说过的,如果已知需要的空间,可以计算 初始化多大容量,能刚好保证不扩容
2. 根据实际业务,调整负载因子
3. 确保hashCode分布均匀
TreeMap (用的比较少)
通过红黑树实现,插入,查找,删除都需要logn的时间复杂度
另外HashMap线程不安全,不要在多线程共享的场景下使用,HashTable虽是线程安全的,但是性能堪忧,HashTable是通过锁全表来保证线程安全的,唯一优点,锁的成本低。可以使用ConcurrentHashMap,
ConcurrentHashMap
java7 采用分段锁,将整个hashmap分为多个Segment,每一个Segment单独内共用这一个锁,一定程度上提高了性能
java8 采用CAS + synchronized桶锁,相较于java7的锁单个segment,粒度更细