一篇文章背会Java集合面试题

1. Java集合的分类

Java集合框架主要分为以下几类:

(1)Map(映射)
  • 特点:存储键值对(Key-Value),Key不可重复,Value可以重复。
  • 常见实现类
    • HashMap:基于哈希表实现,线程不安全。
    • Hashtable:基于哈希表实现,线程安全。
    • ConcurrentHashMap:基于分段锁实现,线程安全且性能优于Hashtable
    • TreeMap:基于红黑树实现,Key有序。
(2)List(列表)
  • 特点:存储有序且可重复的元素。
  • 常见实现类
    • ArrayList:基于动态数组实现,查询快,增删慢。
    • LinkedList:基于双向链表实现,增删快,查询慢。
    • Vector:基于动态数组实现,线程安全。
(3)Set(集合)
  • 特点:存储无序且不可重复的元素。
  • 常见实现类
    • HashSet:基于哈希表实现,元素无序。
    • LinkedHashSet:基于哈希表和链表实现,元素按插入顺序排序。
    • TreeSet:基于红黑树实现,元素有序。
(4)Queue(队列)
  • 特点:先进先出(FIFO)的数据结构。
  • 常见实现类
    • LinkedList:可以用作队列。
    • PriorityQueue:基于堆实现,元素按优先级排序。
    • ArrayDeque:基于数组实现的双端队列。

2. 线程安全与线程不安全的集合

  • 线程安全的集合
    • Vector:线程安全的List
    • Hashtable:线程安全的Map
    • ConcurrentHashMap:线程安全且高效的Map
    • CopyOnWriteArrayList:线程安全的List
    • CopyOnWriteArraySet:线程安全的Set
  • 线程不安全的集合
    • ArrayListLinkedListHashSetHashMap等。

3. ArrayList与LinkedList的异同

相同点
  • 都实现了List接口,存储有序且可重复的元素。
不同点
特性ArrayListLinkedList
底层实现动态数组双向链表
查询性能快(通过索引直接访问)慢(需要从头遍历)
增删性能慢(需要移动元素)快(只需修改指针)
空间占用预留空间,可能浪费每个节点有前后指针,占用更多空间
随机访问支持(实现RandomAccess接口)不支持
适用场景频繁查询,少量增删频繁增删,少量查询

4. ArrayList与Vector的区别

相同点
  • 底层都是动态数组,存储有序且可重复的元素。
不同点
特性ArrayListVector
线程安全线程不安全线程安全
扩容机制扩容为原来的1.5倍扩容为原来的2倍
性能性能较高性能较低(同步开销)
空间占用相对节省空间相对浪费空间

5. ArrayList的扩容机制

  • 初始容量:默认初始容量为10。
  • 扩容规则:当元素数量超过当前容量时,触发扩容。新容量为旧容量的1.5倍。
  • 扩容过程
    1. 创建一个新数组,容量为旧数组的1.5倍。
    2. 将旧数组中的元素复制到新数组中。
    3. 将新数组赋值给ArrayList的内部数组引用。

6. Array与ArrayList的区别

相同点
  • 都可以存储多个元素。
不同点
特性ArrayArrayList
数据类型支持基本数据类型和引用数据类型只支持引用数据类型(需使用包装类)
容量固定长度动态扩容
功能功能简单提供丰富的操作方法(如增删改查)
性能性能较高性能较低(动态扩容和封装开销)

7. 扩展:集合的最佳实践

  • 选择合适的集合:根据需求选择ListSetMapQueue
  • 线程安全:在多线程环境下使用线程安全的集合,如ConcurrentHashMapCopyOnWriteArrayList
  • 性能优化:避免频繁扩容,初始化时指定合适的容量。
  • 遍历集合:使用迭代器或增强for循环遍历集合,避免直接操作底层数据结构。

8. 示例代码

ArrayList与LinkedList
List<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.add(2);
System.out.println(arrayList.get(0)); // 输出 1

List<Integer> linkedList = new LinkedList<>();
linkedList.add(1);
linkedList.add(2);
System.out.println(linkedList.get(0)); // 输出 1
ArrayList扩容
List<Integer> list = new ArrayList<>(5); // 初始容量为5
for (int i = 0; i < 10; i++) {
    list.add(i);
}
System.out.println(list.size()); // 输出 10
Array与ArrayList
int[] array = new int[10]; // 支持基本数据类型
array[0] = 1;

List<Integer> arrayList = new ArrayList<>(); // 只支持引用数据类型
arrayList.add(1);

9. HashMap的底层数据结构

  • JDK 1.7及之前:数组 + 链表。
  • JDK 1.8及之后:数组 + 链表 + 红黑树。
    • 当链表长度超过8且数组长度大于64时,链表会转换为红黑树。
    • 当红黑树节点数小于6时,红黑树会退化为链表。

为什么选择红黑树?

  • 红黑树的查询时间复杂度为O(log n),而链表的查询时间复杂度为O(n)。
  • 红黑树的插入、删除和查找操作相对平衡,适合处理大量数据。

为什么一开始不直接用红黑树?

  • 红黑树的维护成本较高(如左旋、右旋、变色等操作),在数据量较小时,链表的性能已经足够。

10. 解决哈希冲突的方法

  • 链地址法:将哈希冲突的元素存储在链表中(如HashMap)。
  • 开放寻址法:当发生冲突时,寻找下一个空闲位置存储(如ThreadLocalMap)。
  • 再哈希法:使用多个哈希函数,直到找到空闲位置。
  • 建立公共溢出区:将冲突的元素存储在一个单独的溢出区。

11. HashMap的加载因子

  • 默认加载因子:0.75。
  • 为什么是0.75?
    • 加载因子是空间和时间的平衡值。
    • 加载因子过大(如1.0):减少扩容次数,但哈希冲突概率增加,查询性能下降。
    • 加载因子过小(如0.5):减少哈希冲突,但浪费空间,扩容频繁。

12. HashMap的存储索引计算

  • 步骤
    1. 计算keyhashCode
    2. hashCode进行扰动计算((h = key.hashCode()) ^ (h >>> 16)),以减少哈希冲突。
    3. 将扰动后的值与数组长度取模(index = (n - 1) & hash),得到存储索引。

13. HashMap的put流程

  1. 计算key的哈希值,确定存储索引。
  2. 如果该位置为空,直接插入。
  3. 如果该位置不为空:
    • 判断key是否相等(equals方法):
      • 相等:更新value
      • 不相等:遍历链表或红黑树,插入新节点。
  4. 判断是否需要扩容(元素数量超过阈值)。
  5. 判断是否需要将链表转换为红黑树(链表长度超过8且数组长度大于64)。

14. HashMap的key选择

  • 常用key类型StringInteger等。
  • 原因
    • 这些类重写了hashCodeequals方法,保证了哈希值的唯一性和一致性。
    • 这些类是不可变的(Immutable),避免了key被修改导致哈希值变化。

15. HashMap的线程安全问题

  • JDK 1.7及之前
    • 使用头插法,多线程环境下可能导致循环链表,造成死循环。
  • JDK 1.8及之后
    • 改为尾插法,解决了循环链表问题。
    • 但仍然存在数据覆盖问题(多个线程同时插入相同key时,可能导致数据丢失)。

解决方案

  • 使用Collections.synchronizedMap(new HashMap<>())
  • 使用ConcurrentHashMap(推荐)。

16. 扩展:ConcurrentHashMap

  • 特点:线程安全且高效,采用分段锁(JDK 1.7)或CAS + synchronized(JDK 1.8)。
  • 适用场景:高并发环境下的Map操作。

17. 示例代码

HashMap的put操作
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
System.out.println(map.get("a")); // 输出 1
ConcurrentHashMap的使用
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("a", 1);
concurrentMap.put("b", 2);
System.out.println(concurrentMap.get("a")); // 输出 1

18. HashMap总结

  • 数据结构:JDK 1.8之后,HashMap采用数组 + 链表 + 红黑树的结构。
  • 哈希冲突:常用链地址法解决。
  • 加载因子:默认0.75,平衡空间和时间。
  • 线程安全HashMap非线程安全,ConcurrentHashMap是线程安全的替代方案。
  • 最佳实践:选择合适的key类型,避免线程安全问题,合理设置初始容量和加载因子。

ConcurrentHashMap 是 Java 并发编程中非常重要的一个类,常用于高并发场景下的键值对存储。以下是关于 ConcurrentHashMap 的常见面试题及其详细解答,帮助你更好地理解和掌握这一知识点。


19. ConcurrentHashMap 和 HashMap 的区别是什么?

  • 线程安全性
    • HashMap 是非线程安全的,多线程环境下可能导致数据不一致或死循环。
    • ConcurrentHashMap 是线程安全的,支持高并发操作。
  • 实现方式
    • HashMap 使用数组 + 链表 + 红黑树(JDK 1.8)。
    • ConcurrentHashMap 在 JDK 1.7 中使用分段锁(Segment),在 JDK 1.8 中使用 CAS + synchronized
  • 性能
    • HashMap 在单线程下性能更高。
    • ConcurrentHashMap 在多线程下性能更好,避免了全局锁的竞争。

20. ConcurrentHashMap 在 JDK 1.7 和 JDK 1.8 中的实现有什么不同?

  • JDK 1.7
    • 使用分段锁(Segment),将数据分成多个段,每个段独立加锁。
    • 优点:减少了锁的竞争,提高了并发性能。
    • 缺点:实现复杂,内存占用较高。
  • JDK 1.8
    • 取消了分段锁,采用数组 + 链表 + 红黑树的结构。
    • 使用 CASsynchronized 实现线程安全。
    • 优点:简化了实现,提高了并发性能。

21. ConcurrentHashMap 的 get 操作为什么不需要加锁?

  • ConcurrentHashMapget 操作是无锁的,因为:
    • Nodevaluenext 字段都是 volatile 修饰的,保证了可见性。
    • volatile 确保线程能够读取到最新的值,无需加锁。

22. ConcurrentHashMap 的 put 操作是如何保证线程安全的?

  • JDK 1.8
    • 使用 CASsynchronized 实现线程安全。
    • 插入新节点时,使用 CAS 竞争锁。
    • 更新节点时,使用 synchronized 锁定链表或红黑树的头节点。
  • 扩容机制
    • 当元素数量超过阈值时,触发扩容。
    • 扩容时,使用 CASsynchronized 确保线程安全。

23. ConcurrentHashMap 的扩容机制是什么?

  • 触发条件
    • 当元素数量超过阈值(sizeCtl)时,触发扩容。
  • 扩容过程
    • 创建一个新数组,容量为旧数组的 2 倍。
    • 将旧数组中的元素复制到新数组中。
    • 使用 CASsynchronized 确保线程安全。

24. ConcurrentHashMap 如何解决哈希冲突?

  • 链地址法
    • 将哈希冲突的元素存储在链表中。
    • 当链表长度超过 8 且数组长度大于 64 时,链表转换为红黑树。
  • 红黑树
    • 红黑树的查询时间复杂度为 O(log n),适合处理大量数据。

25. ConcurrentHashMap 的 size 方法是如何实现的?

  • JDK 1.7
    • 遍历所有段,累加每个段的元素数量。
    • 由于分段锁的存在,size 方法可能不准确。
  • JDK 1.8
    • 使用 CounterCell 统计元素数量。
    • 通过 CAS 更新 CounterCell,确保线程安全。

26. ConcurrentHashMap 的 key 和 value 是否可以为 null?

  • key 和 value 都不能为 null
    • ConcurrentHashMap 的设计目标是高并发场景,null 值会导致歧义。
    • 如果 keynull,无法确定是 key 不存在还是 keynull
    • 如果 valuenull,无法确定是 value 不存在还是 valuenull

HashMap 中,key 和 value 都可以为 null。这是 HashMapConcurrentHashMap 的一个重要区别。下面我会详细解释这一点,并分析其背后的原因。

  • 示例
    ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
    map.put(null, 1); // 抛出 NullPointerException
    map.put("a", null); // 抛出 NullPointerException
    

26.1 HashMap 的 key 和 value 可以为 null
key 为 null
  • HashMap 允许 keynull
  • keynull 时,HashMap 会将其哈希值计算为 0,并将其存储在数组的第一个位置(索引为 0)。
  • 示例:
    HashMap<String, Integer> map = new HashMap<>();
    map.put(null, 1);
    System.out.println(map.get(null)); // 输出 1
    
value 为 null
  • HashMap 也允许 valuenull
  • 示例:
    HashMap<String, Integer> map = new HashMap<>();
    map.put("a", null);
    System.out.println(map.get("a")); // 输出 null
    
26.2. 为什么 HashMap 允许 key 和 value 为 null?
  • 设计灵活性
    • HashMap 的设计目标是提供一种通用的键值对存储结构,允许 null 值可以满足更多场景的需求。
  • 历史原因
    • HashMap 是 Java 早期引入的类,为了兼容性和易用性,允许 null 值。
  • 与 Hashtable 的区别
    • Hashtable 不允许 keyvaluenull,而 HashMap 允许,这是两者之间的一个重要区别。
26.3. HashMap 中 key 为 null 的实现细节
  • keynull 时,HashMap 会将其哈希值计算为 0,并将其存储在数组的第一个位置(索引为 0)。
  • 源码解析:
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    

26.4. 使用 HashMap 时需要注意的问题
  • key 为 null
    • 如果 keynull,只能有一个 null 键,因为 HashMap 的键是唯一的。
  • value 为 null
    • 如果 valuenull,可以通过 containsKey 方法判断 key 是否存在。

26.5. 示例代码
HashMap<String, Integer> map = new HashMap<>();
map.put(null, 1); // key 为 null
map.put("a", null); // value 为 null

System.out.println(map.get(null)); // 输出 1
System.out.println(map.get("a")); // 输出 null
System.out.println(map.containsKey("a")); // 输出 true

27. ConcurrentHashMap 的迭代器是强一致性还是弱一致性?

  • 弱一致性
    • ConcurrentHashMap 的迭代器不会抛出 ConcurrentModificationException
    • 迭代器反映的是创建迭代器时的状态,后续的修改可能不会反映到迭代器中。

这里涉及到fail-fast和fail-safe的知识点


28. ConcurrentHashMap 的适用场景是什么?

  • 高并发场景
    • 如缓存、计数器、并发任务调度等。
  • 需要线程安全的 Map
    • 如多线程环境下的数据共享。

29. ConcurrentHashMap 的源码分析(JDK 1.8)

put 方法源码解析
public V put(K key, V value) {
    return putVal(key, value, false);
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break;
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key, value);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

ConcurrentHashMap 是 Java 并发编程中非常重要的工具,通过分段锁(JDK 1.7)或 CAS + synchronized(JDK 1.8)实现线程安全。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Nice文棋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值