【面试】集合框架
参考:https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/Java集合框架常见面试题.md
1、说说List,set,Map三者的区别?
List(对付顺序的好帮手):List接口存储一组不唯一有序的对象;
Set(注重独一无二的性质):不允许重复的集合。不会有多个元素引用相同的对象。
Map(用key来搜索的专家):使用键值对存储。Map会维护与key有关联的值,两个key可以引用相同的对象,但key不能重复,典型的key是String类型,但也可以是任何对象。
1、ArrayList与LinkedList的区别?
1)是否线程安全:ArrayList和LinkedList都不是同步的,也就是不保证线程安全;
2)底层数据结构:ArrayList底层使用的是Object数组;LinkedList底层使用的是双向链表数据结构;
3)插入和删除是否受元素位置的影响:
- ArrayList采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。比如,执行add(E e)方法的时候,ArrayList会默认将元素追加到列表的末尾,这种情况的时间复杂度为O(1);但是如果要在指定位置 i 插入和删除元素的话(add(int index,E element)),时间复杂度就为O(n-i)。因为在进行上述操作的时候,集合中第 i 位和第 i 个元素之后的(n-i) 个元素都要执行向后/向前 移一位的操作;
- LinkedList采用链表存储,所以插入、删除元素的时间复杂度不受元素位置的影响,都是近似O(1),而数组为近似O(n)。
4)是否支持快速随机访问:LinkedList不支持高效的随机元素访问,而ArrayList支持。快速随机访问就是通过元素的序号快速获取元素对象(对应 get(int index));
5)内存空间占用:ArrayList的空间浪费主要体现在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(直接前驱+直接后驱+数据)。
补充内容:
List遍历方式选择:
1)实现了RandomAccess接口的list,优先使用普通for循环,其次foreach;
2)未实现RandomAccess接口的list,优先选择 iterator遍历(foreach遍历底层也是通过 iterator实现的),大size的数据,千万不要使用普通的for循环。
补充:双向链表和双向循环链表
2、ArrayList与Vector区别?为什么要用ArrayList取代Vector呢?
Vector类的所有方法都是同步的。可以由两个线程安全的访问一个Vector对象,但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。
ArrayList不是同步的,所以在不需要保证线程安全的时候建议使用ArrayList。
3、ArrayList的扩容机制?
https://www.cnblogs.com/zhexuejun/p/11149406.html
ArrayList的扩容机制就是在添加第一个元素的时候,创建一个长度为10的数组,之后随着元素的增加,以1.5倍原数组的长度创建一个新数组,即10,15,22,33。。。这样的序列建立,将原来的元素拷贝到新数组中,如果数组长度达到上限,则会以MAX_ARRAY_SIZE或者Integer.MAX_VALUE作为最大长度,而多余的元素就会被舍弃掉。
4.HashMap和HashTable的区别?
1)线程是否安全:HashMap是非线程安全的,HashTable是线程安全的;HashTable内部的方法基本都经过synchronized的修饰。(线程安全可以使用ConcurrentHashMap);
2)效率:因为线程安全的问题,HashMap要比HashTable快;另外,HashTable基本被淘汰,代码中不要使用;
3)对Null key 和Null value的支持:HashMap中Null可以作为键,这样的键只有一个,但可以有一个或多个键所对应的值为null。但在HashTable中put进的键值只要有一个null,直接抛出 NullPointException;
4)初始容量大小和每次扩容大小的不同:
- 创建时如果不指定容量初始值,HashTable的默认大小为11,之后每次扩容,容量变为原来的2n+1;HashMap默认的初始化大小为16,之后每次扩容,容量变为原来的2倍;
- 创建时如果给定了容量初始值,,那么HashTable会直接使用你给定的大小;而HashMap会将其扩充为2的幂次方大小,也就说HashMap总是使用2的幂作为Hash表的大小。
5)底层数据结构:JDK1.8以后的HashMap在解决Hash冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜素时间;HashTable没有这样的机制。
5.HashMap与HashSet的区别?
HashSet底层就是基于HashMap实现的。
1)HashMap实现了Map接口;HashSet实现了Set接口;
2)HashMap存储键值对,HashSet仅存储对象;
3)HashMap调用put()方法向Map中添加元素;HashSet调用set()方法向Set中添加元素;
4)HashMap使用键(key)计算HashCode值;HashSet使用成员对象来计算HashCode的值,对于两个对象来说HashCode可能相同,所以equals()方法来判断对象的相等性。
HashSet如何检查重复?
当你把对象加入HashSet时,HashSet会先计算对象的hashCode值来判断对象的加入位置,同时也会与其他加入的对象的hashCode值做比较,如果没有相同的hashCode值,HashSet会假设对象没有重复出现。但如果发现有相同的hashCode值的对象,这时会调用equals()方法来检查hashCode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。
hashCode()与equals()的相关规定:
1)如果两个对象相等,则hashCode一定是相同的;
2)如果两个对象相等,则两个equals()方法返回true;
3)如果两个对象有相同的hashCode值,它们不一定是相等的;
4)综上,如果重写了equals()方法,则一定要重写hashCode()方法。
6.HashMap的底层实现
HashMap的工作原理?
JDK1.8之前HashMap底层是数组和链表结合在一起使用,也就是链表散列。我们使用put(key,value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们调用put()方法的时候,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来存储Entry对象。
当两个对象的hashCode相同会发生什么?
虽然hashCode相同,但在确认是否发生碰撞的之前,hashMap会做equals()比较,如果为true,说明key已存在,直接覆盖;反之按碰撞处理,因为hashCode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用LinkedList存储对象,所以这个Entry会存储在LinkedList中。
如果两个键的hashCode相同,你如何获取值?
我们调用get()方法,HashMap会使用键对象的hashCode找到bucket位置,找到bucket位置后,会调用key.equals()方法去找到LinkedList中的正确节点,最终找到要找的值对象。
你如何减少碰撞,提高效率呢?
主要有两方面,第一是尽量使用String,Integer等不可变,声明为final的类作为键;第二是采用合适的hashCode(),equals()方法。
如果HashMap的大小超过负载因子定义的容量,会发生什么?
默认负载因子为0.75,也就是说,当一个Map填满了75%的bucket时,和其他集合类一样,将会创建原来HashMap两倍大小的bucket数组,并将原来的对象放入到新bucket中,这个过程叫做rehashing。
你了解重新调整HashMap大小存在什么问题吗?
多线程条件下可能会产生条件竞争。当两个线程都发现HashMap需要重新调整大小时,它们都会尝试调整。在调整大小的过程中,存储在LinkedList中的元素次序会反过来,因为移动到新的bucket时,HashMap并不将将元素放在LinkedList的尾部,而是放在头部,这是为了避免尾部遍历。如果发生条件竞争,会发生死循环。多线程情况下,我们一般使用ConcurrentHashMap。
我们可以使用自定义的对象作为键值吗?
当然可以。你可以使用任何对象作为键,只要它符合equals()和hashCode()方法的定义规则,并且将对象插入到Map后不会发生改变。如果这个自定义的对象是不可变的,那么它就满足作为键的条件。
所谓拉链法就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一个格就是一个链表。若遇到哈希冲突,则将冲突的值加入链表中即可。
JDK1.8之后,相较于之前的版本,JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(8)时,将链表转化为红黑树,以减少搜索时间。
7.HashMap多线程操作导致死循环问题?
主要原因在于并发的Rehash会造成元素之间的循环链表。不过jdk1.8后解决了这个问题,但还是不建议在多线程情况下使用HashMap,因为多线程下使用HashMap还是会存在其他问题的,比如数据丢失。并发环境下推荐使用ConcurrentHashMap。
参考:https://coolshell.cn/articles/9606.html
8.ConcurrentHashMap和HashTable的区别?
ConcurrntHashMap和HashTable的区别主要体现在 实现线程安全的方式上。
1)底层数据结构:JDK1.7的ConcurrentHashMap底层采用分段数组+链表实现,JDK1.8采用的数据结构跟HashMap1.8一样,数组+链表/红黑二叉树。HashTable采用的是数组+链表的形式,数组是HashMap的主体,链表则是为了解决Hash冲突而存在的;
2)实现线程安全的方式:
- 在JDK1.7的时候,ConcorrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在所竞争,提高并发访问率。到JDK1.8的时候已经摒弃了Segment概念,而是直接使用node数组+链表+红黑树的数据结构来实现,并发控制使用syncheronized和CAS来操作。整个看起来就像是优化过且线程安全的HashMap;
- HashTable(同一把锁):使用synchronized来保证线程安全,效率低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率会越来越低。
comparable与comparator的区别?
comparable接口实际上是出自java.lang包,它使用compareTo(Object obj)方法来排序;
comparator接口实际上是出自java.util包,它使用compare(Object obj1,Object obj2)方法来排序。
9.集合框架底层数据结构总结:
Collection
1)List
- Array List:Object数组;
- Vector:Object数组;
- LinkedList(有序,输出与输入顺序一致,允许null值):双向链表(JDK1.7之前是循环双向链表);
2)Set
- HashSet(无序,唯一):基于HashMap实现的,底层采用HashMap来保存元素;
- LinkedHashSet(有序):LinkedHashSet继承自HashSet,并且其内部是通过LinkedHashMap来实现的。有点类似于我们之前说的LinkedHashMap;
- TreeSet(有序,唯一):红黑树;
3)Map
- HashMap:JDK1.8之前HashMap由数组+链表组成,数组是HashMap的主体,链表则是为了解决Hash冲突而存在的。JDK1.8之后在解决Hash冲突时有了较大的变化,当链表阈值大于8时,将链表转化为红黑树,以减少搜索时间;
- LinkedHashMap(有序):LinkedHashMap继承自HashMap,所以它的底层与HashMap相同。另外,LinkedHashMap在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑;
- HashTable:数组+链表组成,数组是hashMap的主体,链表则是为了解决哈希冲突而存在的;
- TreeMap:红黑树。
10.如何选用集合?
主要根据集合的特点来选用,比如我们需要根据键值获取元素值时就选用Map接口下的集合;需要排序时就选择TreeMap,不需要排序就选择HashMap,需要保证线程安全就选用ConcurrentHashMap;
当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合,比如TreeSet或HashSet;
不需要就选择实现List接口的集合,比如ArrayList或LinkedList。