一、背景
针对Java基础集合的部分,对一些常见的问题进行整理,方便后续能够随时复习
二、问题与回答
(1)Java集合类ArrayList初始化时数组的默认长度是多少?
答:在new ArrayList() 这段代码执行完后,实际上初始化时的数组的长度是一个空数组,也就是长度为0,我们可以看到源码
DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为一个空数组
只有在执行add方法的时候,才会进行数组长度的初始化,初始化长度为10
(2)ArrayList扩容时扩容多少倍?扩容后是用原来的数组还是新的数组?
答:我们来看源码,扩容的算法是新长度 = 原有长度 + 原有长度 右移运算(10 + 10 >> 1 = 15),有小伙伴可能不清楚右移运算,实际上可以看成整除运算10/2,得到5,所以大概是扩容1.5倍
java.util.ArrayList#grow
第二个问题,扩容后是使用新数组,用的是Arrays.copyOf(elementData, newCapacity)方法,实际上底层用的是System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength))本地方法
(3)ArrayList是一个线程安全的集合类吗?
答:不是,arraylist的add、remove方法从源码上看都没加锁,所以不是线程安全的集合类
(4)判断一个集合类是否为线程安全的机制是什么?
答:这个从我个人理解,有其他意见的小伙伴欢迎在评论区补充
1、集合的增删方法是否增加同步机制(锁机制)
2、通过合理的代码设计,例如Fail-Fast机制,不允许在对集合迭代的时候对集合进行修改前置检查
(5)结合源码说一下Fail-Fast机制
答:Java集合检测机制,Fail-Fast机制是对集合迭代遍历时如果对集合进行修改,则会提示ConcurrentModificationException,实现原理就是通过modCount和expectedModCount两个变量进行对比,如果不相等,则说明你同时对集合进行遍历以及修改,则提示异常.
expectedModCount:是迭代遍历之前则会赋值好,expectedModCount = modCount
modCount:集合增删,会进行加或者减
java.util.ArrayList.Itr#checkForComodification
(6)ArrayList和LinkedList的使用场景。
答:其实这个问题算是一个经典的问题,讲讲ArrayList和LinkedList的特点
ArrayList : 随机插入和删除需要移动数组,查询性能很高,时间复杂度0(1)
LinkedList : 顺序插入和删除只需要改变链表引用即可,查询性能相对低下,时间复杂度O(n/2)
这里说下个人理解
- 如果你使用的场景是顺序插入和删除比较多,查询比较少,那么时候LinkedList较适合
- 如果你使用的场景是查询比较多,那么ArrayList较适合
- 如果随机插入和删除比较多,这个需酌情评估,因为ArrayList是要移动数组,而LinkedList则需要找到原有位置然后再进行插入,两者的性能都不太好
(7)HashMap的底层数据结构。
答:这个也是一个比较经典的问题,HashMap的底层结构是 动态数组 + 链表的结构
(8)HashMap的存储逻辑(put()函数)
答:
- 通过key的hash函数,得到一个哈希值,同时经过异或运算,再进行与运算,确定数组的下标
- 确定下标以后,那就看这个下标有没有给其他元素占据,有,就要加入链表,加入链表之前又要判断是否链表长度超过8,如果超过8又要进行转换为红黑树,没超过8就加入链表,使用尾插法
- 如果确定下标以后,这个下标没有冲突,那就把数组的下标指到这个元素下面。
- put完以后,还会判断hashMap存在的元素是否大于数组长度 * 扩容因子,如果大于就会进行扩容
(8)HashMap存储元素时key完全一样该怎么处理?
答:这个我们通过源码可以看到,value会进行覆盖
(9)HashMap的默认长度是多少?扩容是扩成几倍?
答:看完源码发现在在执行完new HashMap()的时候,实际上只是对一些变量进行初始化,并没有正在的创建动态数组和链表
只有等到put方法第一次执行才会进行动态数组的初始化,长度为16,扩容是2的幂次方,也就是32,64,128,256依次类推
(10)若两个key的hashcode值相同但equals不同,也就是说它们会插入到同一个桶里,新添加的节点是插入到已有元素的前面还是后面?
答:JDK1.7使用头插法,多线程场景下可能会出现循环链表,JDK1.8为了避免这种情况,使用尾插法,则会插入已有元素的后面。
(11)JDK 1.8的HashMap是否线程安全?
答:这个也是比较经典的问题,看源码会发现HashMap的put方法,remove方法没有任何的加锁机制,所以不是线程安全的。
(12)既然HashMap不是线程安全的类,有啥办法解决这个问题?
答:这里需要引出ConcurrentHashMap,它和hashMap的区别是线程安全的Map,底层是基于CAS操作 + synchronized关键字实现锁机制,保证多线程场景下不会出现问题。
(13) ConcurrentHashMap虽然是线程安全的,但它也存在什么问题?
答:从我个人的理解来看,有其他意见的小伙伴也欢迎在评论区补充
- key不能为null
- 因为增加了锁机制,性能会相对没有hashMap那么好
- 复合操作的非原子性:ConcurrentHashMap虽然保证了基本的线程安全,但它并不能保证所有复合操作都是原子性的。例如,一个复合操作可能包括检查某个键是否存在(containsKey)和根据检查结果进行插入或更新(put)。这种由多个基本操作组成的复合操作不是原子性的,因此在多线程环境下可能会遇到竞态条件,导致数据不一致
(13)了解TreeMap吗?TreeMap最大的特点是什么?为什么已经有了HashMap了还要有TreeMap类?
答:TreeMap也是对Map的一种增强,它能够让Map的key按照某种规则进行排序,例如我们要实现排行榜,输出由多到少的排行情况,则可以接触TreeMap进行实现
三、总结
以上总结了关于集合相关的问题,后续遇到新的问题再进行补充,上面的问题都是自己通过对比其他博客的讲解以及梳理源码后进行解答,有着许多个人的理解在里面。这样理解会深刻一点