JAVA:集合框架常见的面试题和答案 不念不忘少年蓝@ 2024-04-20 12:11 81阅读 0赞 #### 1、List接口的常见实现类有哪些? #### 答: 常见的List接口实现类包括: * ArrayList: 基于动态数组实现的List,支持快速随机访问。 * LinkedList: 基于链表实现的List,支持快速的插入和删除操作。 * Vector: 一个线程安全的动态数组,通常不建议使用,可以用ArrayList代替。 * Stack: 继承自Vector,表示一个后进先出(LIFO)的堆栈。 #### 2、ArrayList和LinkedList的区别是什么? #### * 数据结构实现:ArrayList :基于数组,便于按 index 访问,超过数组需要扩容,扩容成本较高。LinkedList:使用链表实现,无需扩容。 * 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。 * 增加和删除效率:在非首尾的增删操作,LinkedList 要比ArrayList 效率要高,因为ArrayList 增删操作要影响数组内的其他数据的下标。 * 内存空间占用:LinkedList 比 ArrayList 更占内存,因为LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。 * 线程安全:ArrayList 和 LinkList 都是不同步的,不保证线程安全。 * 综合来说,需要频繁读取集合中的元素时,更推荐使用 Arrayist,而在增删操作较多时,更推荐使用 LinkedList。 * LinkedList 的双向链表是链表的一种,它的每个数据结点中都有2 个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便的访问它的前驱结点和后继结点。 #### 3、Map接口的常见实现类有哪些? #### 答: 常见的Map接口实现类包括: * HashMap: 基于哈希表实现的Map,提供了快速的查找性能。 * TreeMap: 基于红黑树实现的有序Map,按键的自然顺序或者定制的顺序进行排序。 * LinkedHashMap: 基于哈希表和双向链表实现的Map,可以按照插入顺序或者访问顺序进行遍历。 * Hashtable: 一个线程安全的哈希表,通常不建议使用,可以用HashMap代替。 * ConcurrentHashMap: 一个线程安全的哈希表,支持并发操作。 #### 4、HashTable,HashMap,TreeMap区别? #### * HashTable线程同步,HashMap,TreeMap非线程同步。 * HashTable,TreeMap不允许<键,值>有空值,HashMap允许<键,值>有空值。 * HashTable中hash数组的默认大小是11,增加方式的old\*2+1,HashMap中hash数组的默认大小是16,增长方式一定是2的指数倍。 * TreeMap能够把它保存的记录根据键排序,默认是按升序排序,因此如果你的键需要有序,建议使用TreeMap代替HashMap。 #### 5、加载因子(扩容因子)为何默认是0.75? #### * 在空间占用与查询时间之间取得了较好的权衡 * 大于这个值,空间节省了,但是链表可能过长就会影响性能 * 小于这个值,冲突减少了,但是扩容就会比较频繁,空间占用多并且扩容也会消耗性能 #### 6、LinkedHashMap的应用,底层,原理 #### 答:LinkedHashMap维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序(insert-order)或者是访问顺序,其中默认的迭代访问顺序就是插入顺序,即可以按插入的顺序遍历元素,这点和HashMap有很大的不同。 LRU算法可以用LinkedHashMap实现。 #### 7、HashMap 的底层数据结构是怎样的 ? #### JDK1.8 之前 * JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是链表散列。 * HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到hash 值,然后通过(n-1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。 * 所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。 JDK1.8 之后 * 当链表长度大于阈值(默认为 8)时,会首先调用 treeifyBin()方法。这个方法会根据HashMap 数组来决定是否转换为红黑树。只有当数组长度大于或者等于64的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是执行resize() 方法对数组扩容。 #### 8、HashMap 的扩容机制是怎样的? #### 一般情况下,当元素数量超过阈值时便会触发扩容。每次扩容的容量都是之前容量的2倍。HashMap 的容量是有上限的,必须小于 1<<30,即 1073741824。如果容量超出了这个数,则不再增长,且阈值会被设置为 Integer.MAX\_VALUE。 JDK7 中的扩容机制 * 空参数的构造函数:以默认容量、默认负载因子、默认阈值初始化数组。内部数组是空数组。 * 有参构造函数:根据参数确定容量、负载因子、阈值等。 * 第一次 put 时会初始化数组,其容量变为不小于指定容量的2 的幂数,然后根据负载因子确定阈值。 * 如果不是第一次扩容,则 新容量=旧容量 x 2 ,新阈值=新容量x 负载因子。JDK8 的扩容机制 * 空参数的构造函数:实例化的 HashMap 默认内部数组是null,即没有实例化。第一次调用 put 方法时,则会开始第一次初始化扩容,长度为 16。 * 有参构造函数:用于指定容量。会根据指定的正整数找到不小于指定容量的2的幂数,将这个数设置赋值给阈值(threshold)。第一次调用 put 方法时,会将阈值赋值给容量,然后让 阈值 = 容量 x 负载因子。 * 如果不是第一次扩容,则容量变为原来的 2 倍,阈值也变为原来的2 倍。(容量和阈值都变为原来的 2 倍时,负载因子还是不变)。 此外还有几个细节需要注意: * 首次 put 时,先会触发扩容(算是初始化),然后存入数据,然后判断是否需要扩容; * 不是首次 put,则不再初始化,直接存入数据,然后判断是否需要扩容; #### 9、ConcurrentHashMap 的存储结构是怎样的? #### * Java7 中 ConcurrnetHashMap 使用的分段锁,也就是每一个Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变,默认Segment的个数是 16 个。 * Java8 中的 ConcurrnetHashMap 使用的 Synchronized 锁加CAS 的机制。结构也由Java7 中的 Segment 数组 + HashEntry 数组 + 链表 进化成了Node 数组+链表/红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。 #### 10、用过 ConcurrentHashMap,讲一下他和HashTable 的不同之处? #### * HashTable 就是实现了 HashMap 加上了 synchronized,而ConcurrentHashMap底层采用分段的数组+链表实现,线程安全 * ConcurrentHashMap 通过把整个 Map 分为 N 个 Segment,可以提供相同的线程安全,但是效率提升 N 倍,默认提升 16 倍。 * 并且读操作不加锁,由于 HashEntry 的 value 变量是 volatile 的,也能保证读取到最新的值。 * Hashtable 的 synchronized 是针对整张 Hash 表的,即每次锁住整张表让线程独占,ConcurrentHashMap 允许多个修改操作并发进行,其关键在于使用了锁分离技术 * 扩容:段内扩容(段内元素超过该段对应 Entry 数组长度的75%触发扩容,不会对整个Map 进行扩容),插入前检测需不需要扩容,有效避免无效扩容 #### 11、比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同 #### * HashSet 是 Set 接口的主要实现类 ,HashSet 的底层是HashMap,线程不安全的,可以存储 null 值; * LinkedHashSet 是 HashSet 的子类,能够按照添加的顺序遍历; * TreeSet 底层使用红黑树,元素是有序的,排序的方式有自然排序和定制排序。 #### 12、Comparable 和 Comparator 的区别? #### * comparable 接口实际上是出自 java.lang 包 它有一个 compareTo(Object obj)方法用来排序 * comparator 接口实际上是出自 java.util 包它有一个 compare(Object obj1, Objectobj2)方法用来排序 一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()方法或compare()方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写 compareTo()方法和使用自制的Comparator 方法或者以两个 Comparator 来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 Collections.sort()。 #### 13、什么是哈希冲突? #### 答:当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。 #### 14、CopyOnWriteArrayList与ArrayList、Vector的区别 #### 答:CopyOnWriteArrayList与ArrayList、Vector有以下主要区别: * 线程安全性:CopyOnWriteArrayList是线程安全的,而ArrayList不是;Vector也是线程安全的,但它使用全局锁,导致性能较差。 * 读写性能:CopyOnWriteArrayList具有较高的并发读性能,但写操作性能较差,因为每次写操作都需要复制一个新的副本。ArrayList具有较高的读写性能,但在多线程环境下可能出现线程安全问题。Vector的读写性能较差,因为它使用全局锁。 * 内存占用:CopyOnWriteArrayList在写操作时需要复制一个新的副本,因此可能导致较高的内存占用。ArrayList和Vector的内存占用相对较低。 * 实时性:CopyOnWriteArrayList的迭代器只能获取到写操作前的数据副本,因此在迭代过程中无法获取实时数据。ArrayList和Vector的迭代器可以获取实时数据,但在多线程环境下可能会导致线程安全问题。 #### 15、如何将Map转换为List? #### 答: 可以使用ArrayList的构造函数或者addAll方法将Map转换为List。 使用构造函数: List<Map.Entry<K, V>> list = new ArrayList<>(map.entrySet()); 使用addAll方法: List<Map.Entry<K, V>> list = new ArrayList<>(); list.addAll(map.entrySet()); #### 16、LinkedHashSet和CopyOnWriteArraySet的区别 #### 答:LinkedHashSet和CopyOnWriteArraySet是Java集合框架中不同的实现类,它们具有不同的特性和适用场景。 LinkedHashSet: * 内部实现: LinkedHashSet 是基于哈希表和双向链表实现的,它继承自HashSet,保留了元素插入的顺序。 * 有序性: LinkedHashSet 保持了元素插入的顺序,因此可以按照插入顺序进行遍历。 * 性能: 由于它的底层实现依赖于哈希表,因此查找元素的速度非常快。 * 线程安全: LinkedHashSet 是非线程安全的。 * 适用场景: 当你需要保留元素插入的顺序,并且不需要考虑线程安全时,可以使用LinkedHashSet。 CopyOnWriteArraySet: * 内部实现: CopyOnWriteArraySet 是基于CopyOnWriteArrayList实现的,它是一个线程安全的Set。 * 线程安全: CopyOnWriteArraySet 是线程安全的,它的所有修改操作都是在一个新的副本上进行的,而不会影响原始集合。 * 写时复制: 当需要对集合进行修改时,CopyOnWriteArraySet 会先复制一个新的数组,然后在新的数组上进行修改,最后将新的数组替换原始数组。 * 适用场景: 当你需要在多线程环境下保证安全地对集合进行修改时,可以使用CopyOnWriteArraySet。它适用于读多写少的场景。 #### 17、Java中Queue接口的常见实现类有哪些? #### 答: 常见的Queue接口实现类包括LinkedList、PriorityQueue和ArrayDeque。 #### 18、LinkedList和ArrayDeque之间有什么区别? #### * LinkedList 是一个双向链表实现的队列,可以作为队列和双端队列使用。 * ArrayDeque 是一个基于动态数组实现的双端队列,它的性能比LinkedList稍好,但不支持索引访问。 #### 19、PriorityQueue是什么?它是如何工作的? #### 答: PriorityQueue 是一个优先级队列,它根据元素的优先级来确定出队顺序。默认情况下,PriorityQueue 是一个最小堆实现,可以通过提供一个自定义的Comparator来实现最大堆。 #### 20、队列和栈有什么区别? #### * 队列是一种先进先出(FIFO)的数据结构,只允许在队列的一端插入元素,在另一端删除元素。 * 栈是一种后进先出(LIFO)的数据结构,只允许在栈的一端插入和删除元素。 #### 21、如何在多线程环境中使用BlockingQueue? #### 答案: 可以通过创建一个BlockingQueue实例,然后在生产者线程中使用put方法向队列中添加元素,在消费者线程中使用take方法获取元素。 BlockingQueue<ElementType> queue = new LinkedBlockingQueue<>(); // 生产者线程 queue.put(element); // 消费者线程 ElementType element = queue.take(); #### 22、ConcurrentLinkedQueue和LinkedBlockingQueue有什么区别? #### * ConcurrentLinkedQueue 是一个基于非阻塞算法实现的非阻塞队列,它在高并发环境下表现良好。 * LinkedBlockingQueue 是一个基于锁实现的阻塞队列,它可以在队列为空或者满时阻塞线程。 #### 23、如何实现一个自定义的优先级队列? #### 答案: 可以使用PriorityQueue并提供一个自定义的比较器(Comparator)来实现一个自定义的优先级队列。 PriorityQueue<Element> priorityQueue = new PriorityQueue<>(new MyComparator() #### 24、ArrayBlockingQueue和LinkedBlockingQueue的区别 #### 答:ArrayBlockingQueue和LinkedBlockingQueue是Java中两种不同类型的阻塞队列实现,它们有一些重要的区别。 ArrayBlockingQueue: * 内部实现: ArrayBlockingQueue 是一个基于数组实现的有界阻塞队列,它按照先进先出(FIFO)的原则对元素进行排序。 * 容量限制: ArrayBlockingQueue 在创建时需要指定一个固定的容量,即队列中可以存放的最大元素数量。 * 阻塞特性: 当队列已满时,ArrayBlockingQueue 会阻塞等待直到有空间可以放置新元素。 当队列为空时,尝试从中获取元素的操作会被阻塞等待,直到队列中有新的元素。 * 性能: 由于底层是基于数组实现的,因此在性能上比LinkedBlockingQueue更高效,但它的容量是固定的。 LinkedBlockingQueue: * 内部实现: LinkedBlockingQueue 是一个基于链表实现的无界阻塞队列,它按照先进先出(FIFO)的原则对元素进行排序。 * 容量限制: LinkedBlockingQueue 的容量理论上可以是无限大,因此它不会因为达到最大容量而阻塞。 * 阻塞特性: 当队列为空时,尝试从中获取元素的操作会被阻塞等待,直到队列中有新的元素。 当队列已满时,LinkedBlockingQueue 会阻塞等待直到有空间可以放置新元素。 * 性能: 由于底层是基于链表实现的,相对于ArrayBlockingQueue来说,它在高并发场景下可能会略显低效。 ![在这里插入图片描述][0cf852f73c5e4ec7a3ea753f1a7f4610.png] [0cf852f73c5e4ec7a3ea753f1a7f4610.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/68e3775b347d4389bcece1be98af586f.png
还没有评论,来说两句吧...