Java进阶面试题总结

  1. 1. Java集合
    1. 1.1 Java中常见的集合有哪些?
    2. 1.2 线程安全的集合有哪些?
    3. 1.3 ArrayList和LinkedList的异同点?
    4. 1.4 ArrayList的扩容机制是什么?
    5. 1.5 HashMap的底层数据结构是什么?
    6. 1.6 为什么在解决Hash冲突的时候,不直接用红黑树,而是先用链表,再转红黑树?
    7. 1.7 什么是负载因子?为什么HashMap的默认负载因子为0.75?

Java 进阶面试题总结,涉及集合、JVM、并发等内容,文章将不断更新。

1. Java集合

1.1 Java中常见的集合有哪些?

Java 中的集合主要可以分为四个部分:ListSetMap 和工具类(如 Iterator 迭代器、Enumeration 枚举类、ArraysCollections)。这些集合类主要由两个接口派生而来,即 Collection(包含 ListSetQueue)和 Map,它们是集合框架的根接口。

  • List:一种有序列表的集合,例如,按索引排列的元素的 List。常见的实现类有 ArrayListLinkedListVector
  • Set:一种保证没有重复元素的集合。常见的实现类有 HashSetLinkedHashSetTreeSet
  • Map:一种通过键值(Key-Value)查找的映射表集合。常见的实现类有 HashMapTreeMap

1.2 线程安全的集合有哪些?

Java 中的线程安全集合主要包括以下几种:

  • Vector:这是一个线程安全的动态数组,它提供了与 ArrayList 类似的功能,但每个方法都是同步的,这意味着在多线程环境下,它的性能会比 ArrayList 差。
  • Hashtable:这是一个线程安全的哈希表实现,类似于 HashMap,但是 Hashtable 的方法都是同步的。
  • Stack:这是一个线程安全的栈实现,它继承自 Vector
  • ConcurrentLinkedQueue:这是一个线程安全的队列实现,使用了非阻塞算法。
  • BlockingQueue 接口的实现类,如 ArrayBlockingQueueLinkedBlockingQueue 等。
  • CopyOnWriteArrayListCopyOnWriteArraySet:这是两个线程安全的集合,它们会在修改操作时复制一份数据,避免了修改时的并发问题。
  • ConcurrentHashMap:这是一个线程安全的 HashMap,它通过将数据分段,从而达到并发控制,性能要优于 Hashtable
  • ConcurrentSkipListMap:线程安全且排序的哈希表。

值得注意的是,为了保证集合是线程安全的,相应的效率也比较低;线程不安全的集合效率相对会高一些。如果你的代码只在一个线程中运行,或者多个线程只是读取集合而不修改集合,那么你可以选择线程不安全的集合,因为它们的性能通常会更好。

1.3 ArrayList和LinkedList的异同点?

ArrayListLinkedList 都是 Java 中常用的 List 实现类,它们有一些共同点,也有一些不同点。

共同点:

  • ArrayListLinkedList 都是单列集合中 List 接口的实现类,它们都是存取允许重复,且有序的元素。

不同点:

  • 内部实现:ArrayList 是基于动态数组实现的,底层使用数组来存储元素。而 LinkedList 是基于链表实现的,底层使用双向链表来存储元素。
  • 随机访问:对于随机访问 getset 方法,ArrayList 的速度通常优于 LinkedList,因为 ArrayList 可以根据下标以 O(1) 的时间复杂度对元素进行随机访问,而 LinkedList 的每一个元素都依靠地址指针和它后一个元素连接在一起,查找某个元素的时间复杂度是 O(n)
  • 插入和删除操作:对于插入和删除操作,LinkedList 的速度通常优于 ArrayList,因为当元素被添加到 LinkedList 任意位置的时候,不需要像 ArrayList 那样重新计算大小或者是更新索引。
  • 内存占用:LinkedListArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。而 ArrayList 使用数组来存储元素,因此插入和删除元素时需要移动其他元素占用内存,所以在频繁进行插入和删除操作时,ArrayList 的性能会比较低,且可能会造成内存浪费。

1.4 ArrayList的扩容机制是什么?

ArrayList 的扩容机制是其核心特性之一。在 ArrayList 中添加元素时,如果当前的数组已经满了,那么 ArrayList 会创建一个新的、更大的数组,并将原有数组的元素复制到新的数组中,这个过程就叫做扩容。

具体来说,ArrayList 的扩容机制如下:

  1. 当向 ArrayList 中添加元素时,首先会检查 ArrayList 的当前大小(也就是它内部的数组大小)是否能够容纳新的元素。如果可以,那么新元素就直接被添加到 ArrayList 中。
  2. 如果 ArrayList 的当前大小不足以容纳新的元素,那么 ArrayList 就需要进行扩容操作。在扩容操作中,ArrayList 会创建一个新的数组,新数组的大小是原数组大小的1.5倍,这个值是在 JDK 的源码中定义的。
  3. 接着,ArrayList 会使用 System.arraycopy 方法,将原有数组中的所有元素复制到新的数组中。
  4. 最后,新的数组会替代原有的数组,成为 ArrayList 的内部数组。

值得注意的是,ArrayList 的扩容操作需要重新分配内存空间,并将原来的元素复制到新的数组中,这可能会导致性能问题。因此,在实例化 ArrayList 时设置足够的初始容量,并且尽可能减少数组扩容的次数,可以帮助提高性能:

1
ArrayList<Integer> list = new ArrayList<>(100);

1.5 HashMap的底层数据结构是什么?

HashMap 的底层数据结构主要包括哈希表(数组)、链表和红黑树。

  • 哈希表(数组):HashMap 主要依赖于哈希表来存储数据。哈希表中的每个元素被称为 bucket。数组的每个位置都可以存放一个元素(键值对),数组的索引是通过键的哈希码经过哈希函数计算得来的。这样我们就可以通过键快速定位到数组的某个位置,取出相应的值,这就是 HashMap 快速获取数据的原理。
  • 链表:在理想的情况下,哈希函数将每个键均匀地散列到哈希表的各个位置。但在实际中,我们可能会遇到两个不同的键计算出相同的哈希值,这就是所谓的哈希冲突HashMap 通过使用链表来解决这个问题。当哈希冲突发生时,HashMap 会在冲突的 bucket 位置增加一个链表,新的元素会被添加到链表的末尾。每个链表中的元素都包含了相同哈希值的键值对。
  • 红黑树:从 Java 8 开始,如果链表的长度超过一定的阈值(默认为8),那么链表会被转换为红黑树。红黑树是一种自平衡的二叉查找树,通过保持树的平衡,可以提高查找效率。

1.6 为什么在解决Hash冲突的时候,不直接用红黑树,而是先用链表,再转红黑树?

在解决 Hash 冲突的时候,HashMap 在链表长度大于8的时候才会将链表转换为红黑树,而不是直接使用红黑树,这主要有以下几个原因:

  • 查询效率:红黑树的平均查找长度是 log(n),当链表长度为8时,查找长度为 log(8) = 3,而链表的平均查找长度为 n/2,当长度为8时,平均查找长度为 8 / 2 = 4。因此,当链表长度小于等于8时,使用链表的查询效率其实并不比红黑树差。
  • 插入效率和空间效率:链表的插入操作比红黑树快,且链表的空间占用也比红黑树小。因此,在元素数量较少时,使用链表比红黑树更高效1。
  • 防止频繁转换:如果链表长度在8左右徘徊,且频繁地进行插入和删除操作,可能会导致链表和红黑树之间频繁地转换,这会降低效率。因此,HashMap 设计了两个阈值,链表长度超过8时转为红黑树,少于6时转回链表,这样可以减少转换的频率。

总的来说,这种设计是为了在保证查询效率的同时,尽可能地提高插入效率和空间效率,以及减少因频繁转换而带来的开销。

1.7 什么是负载因子?为什么HashMap的默认负载因子为0.75?

负载因子是用于表示哈希表中元素填满的程度的一个参数。在哈希表(如 Java 的 HashMap)中,负载因子是和扩容机制有关的,当哈希表中的元素个数超过了容量乘以负载因子时,就会进行扩容。例如,如果当前的容器容量是16,负载因子是0.75,16 * 0.75 = 12,也就是说,当容量达到了12的时候就会进行扩容操作。

负载因子的大小对哈希表的性能有重要影响。如果负载因子过大,那么哈希表中的冲突会更频繁,导致查找效率降低。反之,如果负载因子过小,那么哈希表的空间利用率就会降低,导致内存浪费。因此,选择一个合适的负载因子,可以在时间效率和空间效率之间达到一个平衡。在 Java 的 HashMap 中,负载因子的默认值是0.75,这是一个在时间和空间效率之间的折衷选择。