Java集合八股总结
1. Java集合框架体系
首先Java中的集合分为单列集合Collection和双列集合Map,单列集合又分为List集合和Set集合。其中List是有序的,可重复的,Set是无序的,不可重复的,唯一的。List的实现类有Vector、ArrayList、LinkedList。而Set的实现类有HashSet哈希表结构、TreeSet红黑树结构,双列集合Map中包括HashTable,HashMap,ConcurrentHashMap、TreeMap。
2. 算法复杂度分析
大O表示法:不具体表示代码真正的执行时间和空间,而是表示代码执行时间和消耗空间随数据规模增长的变化趋势。
3. List相关面试题
3.1 数组
数组Array是一种用连续的内存空间存储相同数据类型数据的线性数据结构。
只要创建了这个数组,那就会去堆内存中开辟这个空间,用来存放数组,栈内存中存放的是指向首地址的指针。
3.1.1 为什么数组索引从0开始而不是从1开始呢?
因为以0开始时寻址公式如下,寻址公式中,baseAddress是数组的首地址,i是对应的索引值,dataTypeSize是数组元素类型的大小。但是如果是从1开始,寻址公式就变成了如下所示
,变成这样以后,在每次寻址时都要多计算一次减法操作,当数据量大的时候这是一种消耗。
3.1.2 操作数组的时间复杂度(查找)
1. 随机查询,即根据索引查询,数组元素的访问就是通过索引来访问的,计算机通过数组的首个元素的地址以及寻址公式完成查询。所以时间复杂度是O(1)。
2. 未知索引查询,如果从前往后依次遍历,时间复杂度就是O(n),但是二分查找的话就是O(logn)。
3.1.3 操作数组的时间复杂度(插入、删除)
数组是一段连续的内存空间,因此为了保证数组的连续性会使得数组的插入和删除的效率很低。时间复杂度是O(n)。
3.2 ArrayList源码分析
首先是ArrayList中的成员变量:重要的有三个:默认初始的容量:10,Object[] elementData数组,这个数组是真正用来存储数据的;还有一个我们经常使用的size。
接着是ArrayList的构造方法:一个是带初始化容量的构造函数,一个是无参的构造函数,还有一个是Collection对象的构造函数,分别如下:
ArrayList的添加和扩容操作:当第1次添加元素时,需要进行扩容,如果没有指定容量,那就是扩到10。接着再添加元素时就不需要进行扩容,直接添加即可。但是当添加第11个元素的时候,容量不够了,这时候需要去扩容,扩容的方法有两步重要操作:增加原来容量的1.5倍,如由10变为了15;接着就是将原数组拷贝到扩容后的新数组中。
3.3 ArrayList底层实现原理是什么?
首先ArrayList底层是使用动态数组实现的,初始容量为0,当第一次添加元素时扩容为初始容量10,当进行扩容时,容量变为原来的1.5倍,并且每次都需要拷贝数组。ArrayList在添加数组的时候,需要确保已使用长度+1之后足够存储下下一个数据。计算数组的容量,如果当前数组已使用长度+1后大于当前数组的容量,则需要调用扩容方法。确保新增数据有地方存储之后,则将新元素添加到位于size的位置上。最后返回boolean值。
3.4 ArrayList list = new ArrayList(10) 中的list扩容了几次?
当指定集合大小时,执行的是有参构造,在构造的时候就直接创建了对应大小的数组,所以0次扩容。
3.5 如何实现数组和List之间的转换?
首先是数组转换成List:List<String> list = Arrays.asList(strs),调用的是asList方法。该方法的底层是使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把传入的集合进行了包装,最终指向的是同一块地址区域,所以当改变原数组时,这个集合中的元素也会跟着改变。
集合转换为数组:String[] array = list.toArray(new String[list.size()]),使用的是toArray方法,底层是对数组进行拷贝,拷贝完以后就跟原来的数组没什么关系了,所以当原集合改变时,新的数组也不会再发生改变。
3.6 ArrayList与LinkedList的区别是什么?
3.6.1 链表
链表中的每一个元素称为结点,物理存储单元上,是非连续、非顺序的存储结构。
单向链表时间复杂度分析:
查询操作:时间复杂度是O(n);
新增或者删除操作:时间复杂度也是O(n),只要在添加和删除头节点的时候不需要遍历链表,时间复杂度是O(1)。
双向链表,需要额外的两个空间来存储后继节点和前驱节点。
双向链表时间复杂度分析:
查询头尾节点的时间复杂度是O(1),平均的查询时间复杂度是O(n),给定节点找前驱节点的时间复杂度是O(1)。
新增或者删除:删除或新增头尾节点的时间复杂度是O(1),平均的删除或者新增时间复杂度是O(n),给定节点新增或者删除的时间复杂度是O(1)。
3.6.2 ArrayList与LinkedList的区别是什么?
首先说一下这两个底层的数据结构:ArrayList的底层数据结构是动态数组,而LinkedList的底层数据结构是双向链表。
接着说一下操作数据的效率:查询:ArrayList是数组,所以是连续的存储空间,按照下标查询时间复杂度是O(1),即按照索引值就能查询。LinkedList不支持下表查询。无索引情况下,时间复杂度都是O(n)。新增和删除:由于数组新增和删除需要挪动数组,所以时间复杂度是O(n)而双向链表除了头尾节点以外,删除和插入都得查询,所以时间复杂度是O(n)。
接下来是内存空间的占用:ArrayList底层是数组,内存是连续的,节省内存。而LinkedList是双向链表,需要存储数据以及两个指针,比较占用内存。
最后是线程安全方面:ArrayList和LinkedList都不是线程安全的,如果需要保证线程安全有两种方案:在方法内使用,局部变量不涉及到线程安全问题;使用synchronized来包装这两个变量,就是线程安全的了。
4. HashMap相关面试题
4.1 HashMap的实现原理
HashMa底层使用的是数据加链表的结构进行存储的,根据key的值计算hash值,将其值填充到对应数组的索引处,如果两个key计算得到的hash值相同,则称为hash冲突。这时,如果key是一样的,那就会覆盖原来key的值,而如果key不一样,那么就将其添加到该索引处的链表或者红黑树中。即这就是用拉链法解决hash冲突。使用红黑树替代链表的好处就是查询数据时,效率更高,红黑树的复杂度是O(logn),而链表是O(n)。当获取key对应的值时,就是将key的hash值计算出来,去数组中找链表或者红黑树。
4.2 HashMap的jdk1.7和jdk1.8有什么区别?
jdk1.8之前,拉链法解决hash冲突时,就是使用链表,而1.8开始使用链表和红黑树。当链表节点数超过8个时并且数组长度大于等于64,就会将链表转换为红黑树,当扩容时,不足六个,红黑树会自动退回到链表。
4.3 HashMap的put方法的具体流程
根据上图可以将HashMap的put方法的大致流程总结如下:
1. 当第一次添加元素时,首先判断table是否为空,第一次为空,所以初始化长度为16的数组,接着根据key的值计算出该元素在map中对应的位置。如果这个索引处,没有值,则直接将该值插入到数组中即可。如果已经有元素了,那么就判断当前元素即首个元素的key是否跟新元素的key一样,如果一样就直接覆盖value。如果不一样,就去判断当前节点处是链表还是红黑树,如果是链表,就去遍历链表,在遍历过程中,判断当前key是否存在,如果存在则直接赋值。如果不存在,则在链表的尾部插入节点,插入之后需要判断当前链表长度是否已经超过了8,如果是就转换成红黑树,如果没有就结束。如果当前不是链表而是红黑树,则直接在红黑树中进行存储数据即可。所有将元素插入的操作结束之后,都需要去判断现在的size是否已经超出了threshold阈值,如果超过了就得去扩容。threshold = 数组长度*0.75,扩容阈值。数组长度默认为16.
2. 当后边再进行添加元素时,table就不为空,所以直接进行计算key的hash值即可,其他流程与插入第一个元素时相同。
4.4 HashMap的扩容机制
在添加元素或者初始化对的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次扩容都是达到了扩容阈值进行扩容。
扩容时,容量扩大为原来大小的2倍,然后新建一个数组。需要将旧数组中的元素移到新数组中,分三种情况:如果是只有一个元素就直接添加到新数组中即可。如果是红黑树,就按照红黑树的方法添加。如果是链表,则需要遍历链表,可能需要拆分链表,判断e.hash&oldCap是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上。
4.5 hashMap的寻址算法
首先执行hashCode()方法计算该key的哈希值;为了让哈希均匀分布,减少哈希冲突,则进行调用hash()方法进行二次哈希,hashcode值右移16位再异或运算,这样哈希值就更加均匀。计算哈希值以后,需要决定该值应该放在哪个桶里,为了将哈希值映射到桶数组的索引处,计算
hash& (capacity - 1))得到对应的桶索引。
4.6 为何HashMap的数组长度一定是2的次幂?
首先,如果是2的n次幂可以使用位与运算代替取模,从而计算索引时效率更高。其次就是再扩容时重新计算索引效率更高:hash&oldCap==0的元素留在原来位置,否则就新位置等于旧位置+oldCap。