- 如果碰撞,将Node以链表的形式连接在后面
- 如果链表长度超过阈值(8),将链表转化为红黑树,链表长度低于6,则将红黑树转回链表
- 如果节点存在,则替换旧值
- 如果数组快满了(最大容量16*加载因子0.75),就需要resize(扩容两倍)
为什么选择6和8 ?
因为中间7的位置放置频繁的数据结构切换后,影响性能
get方法
- 计算key的hash,在计算index值
- 在数组中查找index值,在比对key值,取出value,复杂度最好是O(1),最坏为O(n)
为什么不直接使用红黑树?
空间和时间的选择,链短的时候空间上占用小,时间还好,转化为红黑树后,便于查找,但是耗费空间。
处理hash冲突的方法有以下几种:
- 开放地址法(线性探测再散列(碰撞后,位置后挪,数组长度+x)x可为正数,二次探测再散列(数组长度+x的平方)x可为正负数,平方后均为正数)
- 再哈希法(多种计算哈希的方法,相同则替换方法,直到算出不重复的哈希值)
- 链地址法(链表)
- 建立公共溢出区(建立一个溢出表,存放冲突的数据)
HashMap的性能慢原因?
- 数据类型自动装箱问题
- resize扩容重新计算index值和hashcode,重新赋值(1.7)
1.8后,扩容位置 = hash值 & 数组长度,如果为0,则不动,反之则反
线程不安全会导致什么
环状链表,resize(扩容)时头插法导致环形链表(1.7版本)
都存在数据丢失的问题数据丢失,1.8版本修复环形链表(尾插)
HashMap中默认容量为什么是2的幂?
因为如果不是2的幂,可能会造成更多的hash碰撞(index 下标碰撞)
假设n为17,n-1的二进制为10000,01001和01101算出的index值均为0
假设n为16,n-1的二进制为01111,01001和01101算出的index值不同
hashcode计算原理
对于int类型,hashcode为它本身,eg:int i = 1; hashcode = 1;
对于对象来说,hashcode是内部地址和对象值的一个映射
hash()算法原理
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
拿到key的hashCode(),在将该值与该值的高16位(h无符号右移16位)进行亦或运算(相同为0,不同为1)
HashTable的理解
put和get方法是用了synchronized修饰,锁住了整个map,同一时刻只有一个线程可以操作
不可以存储null值和null健
SparseArray理解
原理
装箱,int数据类型---->Integer对象,拆箱,Integer对象---->int数据类型
默认容量是10
- key是int值(避免装箱问题),使用二分查找寻找key,同样也是用二分插入,从小到大排列好的
- 两个数组,一组存放key(int []),一组存放value(object [])
mKeys[i] = key;
mValues[i] = value;
- 如果冲突,直接替换value的值
二分插入:
while (lo <= hi) {
//二分法一分而二,数组中间下标
final int mid = (lo + hi) >>> 1;
//二分法一分而二,数组中间下标处的值
final int midVal = array[mid];
if (midVal < value) {
/**
如果数组中间处的值比要找的值小,代表要找的值
在数组的中后部部分,所以当前下标取值为mid + 1
/
lo = mid + 1;
} else if (midVal > value) {
/*
如果数组中间处的值比要找的值大,代表要找的值
在数组的前中部部分,所以当前下标取值为mid - 1
*/
hi = mid - 1;
} else {
//数组中间处的值与要找的值相等,直接返回数组中部的下标mid
return mid; // value found
}
}
第一个值放到最中间位置
第二个值如果大于中间的值放置在左边的中间位置
………….
put方法中,容量充足,计算key值所需存放的index,如果key相同,就直接替换value,如果不同,就insert数组,后续index元素后移,新key放置在index上
较HashMap的优点
- 节省内存
- 性能更好,避免装箱问题
- 数据量不达到千级,key为int值,可以用SparseArray替换HashMap
SparseArray与HashMap的比较,应用场景是?
- SparseArray采用的不是哈希算法,HashMap采用的是哈希算法
- SparseArray采用的是两个一维数组分别用于存储键和值,HashMap采用的是一维数组+单向链表/红黑树
- SparseArray key只能是int类型,而HashMap可以任何类型
- SparseArray key是有序存储(升序),而HashMap不是
- SparseArray 默认容量是10,而HashMap默认容量是16
- SparseArray 内存使用要优于HashMap,因为:
- SparseArray key是int类型,而HashMap是Object
- SparseArray value的存储被不像HashMap一样需要额外的需要一个实体类(Node)进行包装
- SparseArray查找元素总体而言比HashMap要逊色,因为SparseArray查找是需要经过二分法的过程,而HashMap不存在冲突的情况其技术处的hash对应的下标直接就可以取到值
针对上面与HashMap的比较,采用SparseArray还是HashMap,建议根据如下需求选取:
- 如果对内存要求比较高,而对查询效率没什么大的要求,可以是使用SparseArray
- 数量在百级别的SparseArray比HashMap有更好的优势
- 要求key是int类型的,因为HashMap会对int自定装箱变成Integer类型
- 要求key是有序的且是升序
ArrayMap的理解
内部也使用二分算法进行存储和查找,设计上更多考虑了内存中的优化
- int []存储hash值,array[index]存储key,array[index+1]存储value
数据量最好在千级以内
ArrayMap和SparseArray怎么进行选取?
- 如果key为int,那么选取SparseArray进行存储, 不存在封/拆箱问题
- 如果key不为int,则使用ArrayMap
TreeMap的理解
TreeMap是一个二叉树的结构,红黑树
不允许重复的key
TreeMap没有调优选项,因为其红黑树总保持在平衡状态
TreeMap和HashMap的区别?
- TreeMap由红黑树构成,HashMap由数组+链表/红黑树构成
- HashMap元素没有顺序,TreeMap元素会根据可以进行升序排序
- HashMap进行插入,查找,删除最好,TreeMap进行自然顺序便利或者自定义顺序便利比较好
ThreadLocal的理解
面试官:小伙子,听说你看过ThreadLocal源码?(万字图文深度解析ThreadLocal)
线程隔离,数据不交叉
- ThreadLocalMap,每个thread都存在一个变量ThreadLocalMap threadLocals
- threadLocalMap中存在Entry,同ThreadLocal之间为弱引用关系
- ThreadLocalMap中key为ThreadLocal的弱引用,value为Entry,内部为一个object对象
- table默认大小为16,存在初始容量(16)和阈值(16*2/3)
- 在ThreadLocal中使用get()和set()方法初始化threadLocals
- get、set、remove方法将key==null的数据清除
- table是环形数组
线性探测法避免哈希冲突,增量查找没有被占用的地方
通过hashcode计算索引位置,如果key值相同,则替换,不同就nextIndex,继续判断,直到插入数据
ThreadLocal就是管理每个线程中的ThreadLocalMap,所以线程隔离了。
ThreadLocalMap的理解
新建ThreadLcoal的时候,创建一个ThreadLocalMap对象,计算hash的时候使用0x61c88647这个值,他是黄金分割数,导致计算出来的hash值比较均匀,这样回大大减少hash冲突,内部在采用线性探测法解决冲突 set:
- 根据key计算出数组索引值
- 遍历该索引值的链表,如果为空,直接将value赋值,如果key相等,直接更新value,如果key不相等,使用线性探测法再次检测。
ThreadLocal使用弱引用的原因
key使用了弱引用,如果key使用强引用,那么当ThreadLocal的对象被回收了,但ThreadLocalMap还持有ThreadLocal的强引用,回导致ThreadLocal不会被回