Java集合框架核心体系

Java集合框架(Java Collections Framework, JCF)是Java标准库中最重要的组成部分之一, 它提供了一套性能优良、使用方便的接口和类,用于存储和处理对象集合。 Java集合面试题中,集合框架的基础知识往往是第一道门槛, 理解其整体架构对于深入理解后续的高级特性至关重要。

Collection接口

Collection是集合框架的根接口(除Map外),定义了集合的基本操作, 如add、remove、contains、size等。它是List、Set、Queue等子接口的父接口。

Map接口

Map接口用于存储键值对(Key-Value),虽然不属于Collection层次, 但同样是Java集合框架的核心。常见的实现类包括HashMap、TreeMap、LinkedHashMap等。

Iterator迭代器

迭代器是遍历集合的标准方式,通过Iterator接口提供next()、hasNext()、remove()方法, 实现了统一访问集合元素的方式,支持快速失败(fail-fast)机制。

集合框架类图结构

掌握集合框架的继承关系是回答Java集合面试题的基础。 以下是主要的继承关系梳理:

接口 主要实现类 特点 线程安全
List ArrayList, LinkedList, Vector 有序、可重复、支持索引 Vector是线程安全的
Set HashSet, TreeSet, LinkedHashSet 无序(HashSet)、有序(TreeSet)、不可重复 非线程安全
Queue PriorityQueue, LinkedList 先进先出、优先级队列 非线程安全
Map HashMap, TreeMap, LinkedHashMap 键值对存储、键不可重复 非线程安全

Map系列高频面试题

Map集合是Java集合面试题中的重中之重,尤其是HashMap, 几乎每场面试都会涉及。以下是对HashMap及相关Map实现类的深度解析。

1. HashMap底层数据结构是什么?

在JDK 1.7及之前,HashMap由数组+链表组成,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。 在JDK 1.8及之后,引入了红黑树结构,当链表长度超过8且数组长度超过64时,链表会转换为红黑树, 以提高查询效率。红黑树的平均查询时间复杂度为O(log n),而链表为O(n)。

// JDK 1.8 HashMap核心数据结构
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    // 如果是红黑树节点
    TreeNode<K,V> parent;
    TreeNode<K,V> left;
    TreeNode<K,V> right;
}

2. HashMap的哈希冲突如何解决?

HashMap采用链地址法(Separate Chaining)解决哈希冲突。 当两个不同的键计算出的哈希值相同时,它们会被存储在同一个桶(bucket)的链表中。 在JDK 1.8中,如果链表长度超过8,且数组长度超过64,链表会转换为红黑树, 从而将最坏情况下的查询时间从O(n)降低到O(log n)。

3. HashMap的扩容机制是怎样的?

HashMap的初始容量为16,负载因子为0.75。当元素个数超过容量×负载因子时, HashMap会进行扩容,容量变为原来的2倍(必须是2的幂)。 扩容时需要重新计算所有元素的哈希值,并将其重新分配到新的桶中。 在JDK 1.8中,扩容时可以通过判断高位是否为0来决定元素是留在原位置还是移动到原位置+旧容量的位置, 从而避免了重新计算哈希值。

4. HashMap与HashTable的区别?

特性 HashMap HashTable
线程安全 非线程安全 线程安全
允许null键/值 允许 不允许
性能 较高 较低(全表锁)
迭代器 fail-fast fail-fast

List系列高频面试题

List接口是有序的集合,允许重复元素。在Java集合面试题中, ArrayList和LinkedList的区别是经典问题,需要从底层数据结构、增删改查性能、内存占用等方面进行对比。

1. ArrayList与LinkedList的区别?

ArrayList
LinkedList
对比总结

ArrayList特点

ArrayList基于动态数组实现,支持随机访问,查询效率高(O(1))。 但在中间位置插入或删除元素时,需要移动大量元素,效率较低(O(n))。 内存占用相对较小,因为只需要存储元素本身。

// ArrayList核心数组
transient Object[] elementData;

LinkedList特点

LinkedList基于双向链表实现,不支持随机访问,查询效率低(O(n))。 但在头尾插入或删除元素时,效率很高(O(1))。 内存占用较大,因为每个节点还需要存储前驱和后继指针。

// LinkedList节点结构
private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;
}

对比总结

  • <strong>查询操作</strong>:ArrayList优于LinkedList
  • <strong>插入/删除操作</strong>:LinkedList在头尾操作时优于ArrayList
  • <strong>内存占用</strong>:ArrayList更节省内存
  • <strong>使用场景</strong>:频繁查询选ArrayList,频繁增删选LinkedList

2. ArrayList的扩容机制?

ArrayList的初始容量为10。当添加元素超过当前容量时,会进行扩容。 扩容策略为:新容量 = 旧容量 + 旧容量/2(即1.5倍)。 例如,从10扩容到15,从15扩容到22,依此类推。 如果指定了初始容量,则按指定容量初始化。

Set系列高频面试题

Set接口是不允许重复元素的集合。HashSet、TreeSet、LinkedHashSet是Set接口的三个主要实现类, 它们在Java集合面试题中也经常被考察。

1. HashSet如何保证元素唯一性?

HashSet底层基于HashMap实现,利用HashMap的Key不可重复特性来保证Set元素的唯一性。 当向HashSet中添加元素时,会调用元素的hashCode()方法计算哈希值, 然后根据哈希值确定存储位置。如果该位置已有元素,则调用equals()方法比较, 如果相等则认为元素重复,不添加;否则以链表或红黑树形式存储。

2. HashSet与TreeSet的区别?

HashSet

基于HashMap实现,无序,不允许重复。 查询、插入、删除效率高(O(1))。 适用于不需要排序的场景。

TreeSet

基于TreeMap实现,有序(自然排序或定制排序),不允许重复。 查询、插入、删除效率为O(log n)。 适用于需要排序的场景。

并发集合面试题

在高并发场景下,线程安全的集合类是Java集合面试题的难点和重点。 ConcurrentHashMap是面试中的常客,需要深入理解其在不同JDK版本中的实现差异。

1. ConcurrentHashMap在JDK 1.7和1.8中的区别?

JDK 1.7
Segment分段锁机制

ConcurrentHashMap采用Segment数组+HashEntry数组+链表的结构。 Segment继承自ReentrantLock,每个Segment保护一个HashEntry数组。 锁粒度为Segment级别,默认16个Segment,并发度为16。

JDK 1.8
Node数组+链表+红黑树+CAS+synchronized

取消了Segment,直接使用Node数组+链表+红黑树。 使用CAS+synchronized进行同步,锁粒度细化到桶级别。 并发性能更高,支持更高的并发度。

2. CopyOnWriteArrayList的特点?

CopyOnWriteArrayList是一种线程安全的List实现,适用于读多写少的场景。 其核心思想是"写入时复制"(Copy-On-Write),每次修改操作都会创建一个新的数组副本, 修改完成后将新数组替换旧数组。这样读操作不需要加锁,性能很高,但写操作成本较大。

核心源码解析

深入理解源码是应对Java集合面试题的关键。 以下是对HashMap和ConcurrentHashMap核心方法的源码级解析。

1. HashMap的put方法流程

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 1. 如果数组为空或长度为0,则初始化数组
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2. 计算索引位置,如果该位置为空,直接插入
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 3. 如果该位置已有元素,判断key是否相同
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            // 4. 如果是红黑树节点,调用树插入方法
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 5. 如果是链表,遍历链表
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 6. 如果链表长度超过8,转换为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 7. 如果key已存在,更新value
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 8. 如果元素个数超过阈值,扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

2. ConcurrentHashMap的put方法流程(JDK 1.8)

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;
        // 1. 如果数组为空,初始化数组
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 2. 如果计算位置为空,使用CAS插入
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                break;
        }
        // 3. 如果该位置节点hash为MOVED,说明正在扩容,协助扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 4. 使用synchronized锁定桶头节点
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    // 5. 如果是链表节点,遍历插入
                    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, null);
                                break;
                            }
                        }
                    }
                    // 6. 如果是红黑树节点,调用树插入方法
                    else if (f instanceof TreeNode)
                        ((TreeNode<K,V>)f).putTreeVal(this, tab, hash, key, value);
                }
            }
            // 7. 如果链表长度超过8,转换为红黑树
            if (binCount >= 0) {
                if (binCount >= TREEIFY_THRESHOLD - 1)
                    treeifyBin(tab, hash);
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

网友还关心:Java集合高频问答

除了上述核心面试题外,以下问题也是Java集合面试题中经常被提及的周边知识, 建议开发者重点关注。

为什么重写equals()必须重写hashCode()?

因为HashMap等基于哈希的集合依赖hashCode()来确定存储位置, 如果只重写equals()而不重写hashCode(),会导致两个相等的对象可能具有不同的哈希值, 从而无法正确存储和检索。

Iterator的remove()方法有什么作用?

Iterator的remove()方法用于在遍历过程中安全地删除当前元素, 避免ConcurrentModificationException异常。 直接调用集合的remove()方法在遍历过程中是不安全的。

为什么推荐使用包装类而不是基本数据类型?

Java集合框架只能存储对象,不能存储基本数据类型。 使用包装类可以进行null值表示,便于区分未初始化和值为0的情况。 同时,包装类提供了更多实用的方法。

如何遍历HashMap并删除元素?

推荐使用Iterator的remove()方法,或者使用Java 8的forEachRemaining()结合过滤条件。 直接在增强for循环中调用remove()会导致ConcurrentModificationException。

其他相关问题

  • <strong>WeakHashMap的特点?</strong>:键是弱引用,当键不再被其他强引用指向时,会被垃圾回收器回收。
  • <strong>LinkedHashMap的访问顺序?</strong>:可以配置为按访问顺序排序,适用于实现LRU缓存。
  • <strong>EnumSet的特点?</strong>:专为枚举类型设计的Set实现,内部使用位向量,性能极高。
  • <strong>PriorityQueue的排序方式?</strong>:基于堆实现,支持自定义比较器,按优先级出队。