面试专题:集合框架
面试专题:集合框架
核心理论
1.1 集合框架体系结构
Java集合框架主要分为三大体系:Collection、Map和工具类。Collection接口下有List、Set和Queue三大分支,Map接口则提供键值对存储能力。
1.2 集合框架核心接口对比
| 接口 | 特点 | 实现类 | 线程安全 | 
|---|---|---|---|
| List | 有序可重复 | ArrayList, LinkedList, Vector | Vector是,其余否 | 
| Set | 无序不可重复 | HashSet, TreeSet, LinkedHashSet | 均否 | 
| Queue | 先进先出 | ArrayDeque, LinkedList, PriorityQueue | 均否 | 
| Map | 键值对映射 | HashMap, TreeMap, LinkedHashMap | Hashtable是,其余否 | 
1.3 面试高频考点分类
- 基础概念题:集合体系结构、实现原理、特性对比
- 源码分析题:HashMap、ConcurrentHashMap、ArrayList等实现细节
- 算法设计题:基于集合的算法实现与优化
- 性能分析题:时间/空间复杂度分析、性能优化策略
- 并发编程题:线程安全集合、并发修改异常处理
代码实践
2.1 集合初始化与遍历方式
2.1.1 常见集合初始化
/**
 * 集合初始化方式对比
 * JDK 8及以上版本特性
 */
public class CollectionInitialization {
    public static void main(String[] args) {
        // 1. 传统初始化方式
        List<String> list1 = new ArrayList<>();
        list1.add("Java");
        list1.add("Python");
        list1.add("C++");
        // 2. 双括号初始化(匿名内部类,存在内存泄漏风险)
        List<String> list2 = new ArrayList<String>() {{
            add("Java");
            add("Python");
            add("C++");
        }};
        // 3. JDK 8 Stream API初始化
        List<String> list3 = Stream.of("Java", "Python", "C++").collect(Collectors.toList());
        // 4. JDK 9+ of()方法(不可变集合)
        List<String> list4 = List.of("Java", "Python", "C++");
        Set<String> set1 = Set.of("Java", "Python", "C++");
        Map<String, Integer> map1 = Map.of("Java", 1, "Python", 2, "C++", 3);
        // 5. Guava库初始化(需引入第三方依赖)
        List<String> list5 = Lists.newArrayList("Java", "Python", "C++");
        Set<String> set2 = Sets.newHashSet("Java", "Python", "C++");
        Map<String, Integer> map2 = Maps.newHashMap();
    }
}2.1.2 集合遍历方式性能对比
/**
 * 集合遍历方式及性能分析
 * 结论:ArrayList下标遍历最快,LinkedList迭代器遍历最快
 */
public class CollectionTraversal {
    public static void main(String[] args) {
        List<String> arrayList = new ArrayList<>();
        List<String> linkedList = new LinkedList<>();
        // 添加测试数据
        for (int i = 0; i < 100000; i++) {
            arrayList.add("element" + i);
            linkedList.add("element" + i);
        }
        // ArrayList遍历方式
        long start = System.currentTimeMillis();
        // 1. 下标遍历
        for (int i = 0; i < arrayList.size(); i++) {
            String element = arrayList.get(i);
        }
        System.out.println("ArrayList下标遍历: " + (System.currentTimeMillis() - start) + "ms");
        // 2. 增强for循环
        start = System.currentTimeMillis();
        for (String element : arrayList) {
            // do nothing
        }
        System.out.println("ArrayList增强for循环: " + (System.currentTimeMillis() - start) + "ms");
        // 3. 迭代器遍历
        start = System.currentTimeMillis();
        Iterator<String> iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            String element = iterator.next();
        }
        System.out.println("ArrayList迭代器遍历: " + (System.currentTimeMillis() - start) + "ms");
        // LinkedList遍历方式(测试略,结论:迭代器遍历性能最优)
    }
}2.2 高频面试题实现
2.2.1 HashMap手写实现(简化版)
/**
 * 简化版HashMap实现
 * 包含put、get核心方法,拉链法解决哈希冲突
 */
public class SimpleHashMap<K, V> {
    // 默认初始容量
    private static final int DEFAULT_CAPACITY = 16;
    // 负载因子
    private static final float LOAD_FACTOR = 0.75f;
    // 数组(桶)
    private Entry<K, V>[] table;
    // 元素数量
    private int size;
    @SuppressWarnings("unchecked")
    public SimpleHashMap() {
        table = new Entry[DEFAULT_CAPACITY];
        size = 0;
    }
    /**
     * 哈希函数:计算键的哈希值并映射到数组索引
     */
    private int hash(K key) {
        return key == null ? 0 : Math.abs(key.hashCode()) % table.length;
    }
    /**
     * 添加键值对
     */
    public void put(K key, V value) {
        // 计算哈希值和索引
        int index = hash(key);
        // 遍历链表查找是否存在相同key
        Entry<K, V> entry = table[index];
        while (entry != null) {
            if ((key == null && entry.key == null) || (key != null && key.equals(entry.key))) {
                // 键存在,更新值
                entry.value = value;
                return;
            }
            entry = entry.next;
        }
        // 键不存在,添加新节点(头插法)
        Entry<K, V> newEntry = new Entry<>(key, value, table[index]);
        table[index] = newEntry;
        size++;
        // 检查是否需要扩容
        if (size >= table.length * LOAD_FACTOR) {
            resize();
        }
    }
    /**
     * 获取键对应的值
     */
    public V get(K key) {
        int index = hash(key);
        Entry<K, V> entry = table[index];
        while (entry != null) {
            if ((key == null && entry.key == null) || (key != null && key.equals(entry.key))) {
                return entry.value;
            }
            entry = entry.next;
        }
        return null;
    }
    /**
     * 扩容方法
     */
    @SuppressWarnings("unchecked")
    private void resize() {
        Entry<K, V>[] oldTable = table;
        int newCapacity = oldTable.length * 2;
        Entry<K, V>[] newTable = new Entry[newCapacity];
        // 重新哈希并转移所有元素
        for (int i = 0; i < oldTable.length; i++) {
            Entry<K, V> entry = oldTable[i];
            while (entry != null) {
                Entry<K, V> next = entry.next;
                int newIndex = hash(entry.key);
                entry.next = newTable[newIndex];
                newTable[newIndex] = entry;
                entry = next;
            }
        }
        table = newTable;
    }
    /**
     * 键值对节点类
     */
    static class Entry<K, V> {
        K key;
        V value;
        Entry<K, V> next;
        Entry(K key, V value, Entry<K, V> next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }
    // Getter方法
    public int size() {
        return size;
    }
}2.2.2 集合去重与排序
/**
 * 集合去重与排序综合案例
 */
public class CollectionDeduplicationAndSorting {
    public static void main(String[] args) {
        // 1. 数组去重并排序
        Integer[] numbers = {3, 1, 2, 5, 3, 7, 2, 8, 5};
        // 去重
        Set<Integer> numberSet = new HashSet<>(Arrays.asList(numbers));
        // 排序
        List<Integer> sortedList = new ArrayList<>(numberSet);
        Collections.sort(sortedList);
        System.out.println("去重并排序结果: " + sortedList); // [1, 2, 3, 5, 7, 8]
        // 2. 自定义对象去重与排序
        List<Person> persons = Arrays.asList(
            new Person("Alice", 25),
            new Person("Bob", 30),
            new Person("Alice", 25), // 重复对象
            new Person("Charlie", 20)
        );
        // 去重(需重写Person类的equals和hashCode方法)
        Set<Person> uniquePersons = new HashSet<>(persons);
        // 排序(按年龄升序,年龄相同按姓名升序)
        List<Person> sortedPersons = new ArrayList<>(uniquePersons);
        sortedPersons.sort(Comparator.comparingInt(Person::getAge)
            .thenComparing(Person::getName));
        System.out.println("自定义对象排序结果: " + sortedPersons);
    }
    static class Person {
        private String name;
        private int age;
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
        // Getters
        public String getName() { return name; }
        public int getAge() { return age; }
        // 重写equals和hashCode方法以支持去重
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Person person = (Person) o;
            return age == person.age && Objects.equals(name, person.name);
        }
        @Override
        public int hashCode() {
            return Objects.hash(name, age);
        }
        @Override
        public String toString() {
            return name + "(" + age + ")";
        }
    }
}设计思想
3.1 集合框架设计模式应用
3.1.1 迭代器模式(Iterator)
迭代器模式提供了一种顺序访问集合元素的方法,而无需暴露集合的内部实现。
/**
 * 迭代器模式在集合框架中的应用
 */
public class IteratorPatternDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
        Set<String> set = new HashSet<>(Arrays.asList("X", "Y", "Z"));
        // 使用迭代器遍历集合
        iterateCollection(list.iterator());
        iterateCollection(set.iterator());
    }
    /**
     * 通用迭代器遍历方法
     * 体现了迭代器模式的多态性
     */
    private static void iterateCollection(Iterator<?> iterator) {
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}3.1.2 装饰器模式(Decorator)
装饰器模式允许向一个现有对象添加新的功能,同时又不改变其结构。Collections工具类中的synchronizedXXX方法就是典型应用。
/**
 * 装饰器模式在集合框架中的应用
 * 通过Collections.synchronizedList实现线程安全集合
 */
public class DecoratorPatternDemo {
    public static void main(String[] args) {
        // 创建普通ArrayList
        List<String> unsafeList = new ArrayList<>();
        // 使用装饰器模式包装为线程安全集合
        List<String> safeList = Collections.synchronizedList(unsafeList);
        // 多线程操作安全集合
        ExecutorService executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 1000; i++) {
            int index = i;
            executor.submit(() -> safeList.add("element" + index));
        }
        executor.shutdown();
        try {
            executor.awaitTermination(1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("安全集合大小: " + safeList.size()); // 应该为1000
    }
}3.2 集合选择策略
在实际开发和面试中,选择合适的集合类型至关重要,需考虑以下因素:
避坑指南
4.1 集合使用常见错误
4.1.1 ConcurrentModificationException异常
当使用增强for循环遍历集合的同时修改集合结构(添加/删除元素)时,会抛出此异常。
/**
 * ConcurrentModificationException异常演示与解决方案
 */
public class ConcurrentModificationDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
        // 错误示例:增强for循环中删除元素
        try {
            for (String element : list) {
                if (element.equals("B")) {
                    list.remove(element); // 会抛出ConcurrentModificationException
                }
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("捕获异常: " + e.getMessage());
        }
        // 正确解决方案1:使用迭代器
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String element = iterator.next();
            if (element.equals("B")) {
                iterator.remove(); // 使用迭代器的remove方法
            }
        }
        System.out.println("迭代器删除后: " + list);
        // 正确解决方案2:使用Stream API(JDK 8+)
        List<String> newList = list.stream()
            .filter(element -> !element.equals("C"))
            .collect(Collectors.toList());
        System.out.println("Stream过滤后: " + newList);
    }
}4.1.2 集合转换与不可变性问题
/**
 * 集合转换与不可变性问题
 */
public class CollectionImmutabilityDemo {
    public static void main(String[] args) {
        // 问题1:Arrays.asList返回的List不可修改
        List<String> fixedSizeList = Arrays.asList("A", "B", "C");
        try {
            fixedSizeList.add("D"); // 会抛出UnsupportedOperationException
        } catch (UnsupportedOperationException e) {
            System.out.println("Arrays.asList返回的列表不可添加元素");
        }
        // 解决方案:包装为可修改的ArrayList
        List<String> modifiableList = new ArrayList<>(Arrays.asList("A", "B", "C"));
        modifiableList.add("D"); // 正常执行
        // 问题2:JDK 9+ List.of()返回的是不可变集合
        List<String> immutableList = List.of("X", "Y", "Z");
        try {
            immutableList.set(0, "W"); // 会抛出UnsupportedOperationException
        } catch (UnsupportedOperationException e) {
            System.out.println("List.of()返回的列表不可修改");
        }
        // 创建真正不可变的集合
        List<String> trulyImmutableList = Collections.unmodifiableList(new ArrayList<>(Arrays.asList("1", "2", "3")));
        try {
            trulyImmutableList.add("4"); // 会抛出UnsupportedOperationException
        } catch (UnsupportedOperationException e) {
            System.out.println("unmodifiableList返回的列表不可修改");
        }
    }
}4.2 性能优化建议
4.2.1 初始容量设置
为集合设置合适的初始容量可以减少扩容次数,提高性能。
/**
 * 集合初始容量优化
 */
public class CollectionCapacityOptimization {
    public static void main(String[] args) {
        // 已知大概元素数量时,指定初始容量
        int expectedSize = 10000;
        
        // ArrayList:初始容量设置为expectedSize,避免多次扩容
        List<String> optimizedList = new ArrayList<>(expectedSize);
        
        // HashMap:初始容量设置为(expectedSize / 0.75f) + 1,避免扩容
        int initialCapacity = (int) (expectedSize / 0.75f) + 1;
        Map<String, Integer> optimizedMap = new HashMap<>(initialCapacity);
    }
}4.2.2 集合工具类使用陷阱
/**
 * 集合工具类使用注意事项
 */
public class CollectionUtilsPitfalls {
    public static void main(String[] args) {
        // 1. Collections.emptyList()返回的是不可变列表
        List<String> emptyList = Collections.emptyList();
        try {
            emptyList.add("A"); // 抛出UnsupportedOperationException
        } catch (UnsupportedOperationException e) {
            System.out.println("emptyList不可修改");
        }
        // 2. Collections.synchronizedList并非绝对线程安全
        List<String> syncList = Collections.synchronizedList(new ArrayList<>());
        // 迭代操作仍需手动加锁
        synchronized (syncList) {
            Iterator<String> iterator = syncList.iterator();
            while (iterator.hasNext()) {
                System.out.println(iterator.next());
            }
        }
        // 3. 使用subList时的注意事项
        List<String> originalList = new ArrayList<>(Arrays.asList("A", "B", "C", "D", "E"));
        List<String> subList = originalList.subList(1, 4); // [B, C, D]
        subList.set(0, "X"); // 修改子列表会影响原列表
        System.out.println(originalList); // [A, X, C, D, E]
        
        originalList.add("F"); // 原列表结构修改后,子列表操作会抛出ConcurrentModificationException
        try {
            System.out.println(subList.size());
        } catch (ConcurrentModificationException e) {
            System.out.println("原列表结构修改后子列表失效");
        }
    }
}深度思考题
思考题1:HashMap在JDK 7和JDK 8中的实现差异及性能影响
思考题回答:HashMap在JDK 7和JDK 8中的主要实现差异:
- 数据结构: - JDK 7:数组 + 链表
- JDK 8:数组 + 链表 + 红黑树(当链表长度超过阈值8时,链表转为红黑树)
 
- 哈希计算: - JDK 7:扰动函数进行4次位运算
- JDK 8:简化为一次位运算(key.hashCode() ^ (key.hashCode() >>> 16))
 
- 扩容机制: - JDK 7:头插法,可能导致多线程环境下的循环链表问题
- JDK 8:尾插法,解决了循环链表问题
 
- 性能影响: - JDK 8在哈希冲突严重时(链表较长)查询性能更优,从O(n)提升到O(log n)
- JDK 8的哈希计算更高效,减少了位运算次数
- JDK 8的扩容过程更稳定,避免了头插法可能导致的并发问题
 
实际应用建议:优先使用JDK 8及以上版本,对于大数据量且哈希冲突可能性高的场景,HashMap性能优势更明显。
思考题2:如何实现一个线程安全的HashMap?对比ConcurrentHashMap的实现原理
思考题回答:实现线程安全HashMap的方案及对比:
- 使用Collections.synchronizedMap: - 原理:对HashMap的所有方法添加synchronized同步锁
- 优点:实现简单,完全线程安全
- 缺点:性能差,多线程竞争同一把锁,并发度低
 
- 使用ReentrantLock手动加锁: - 原理:在操作HashMap前后手动加锁和解锁
- 优点:可灵活控制锁粒度
- 缺点:实现复杂,需手动处理异常和锁释放
 
- ConcurrentHashMap实现: - JDK 7:分段锁(Segment)机制,将HashMap分为多个段,每个段独立加锁
- JDK 8:CAS + synchronized,锁粒度细化到链表头节点或红黑树的根节点
- 优点:并发度高,读写性能好,支持部分并发操作
- 缺点:实现复杂
 
性能对比:ConcurrentHashMap > ReentrantLock手动加锁 > Collections.synchronizedMap
实际应用建议:优先使用ConcurrentHashMap,在JDK 8及以上版本中,其性能已接近HashMap,同时提供了良好的线程安全性。
思考题3:集合框架在JDK 1.8到JDK 21的演进及新特性
思考题回答:JDK 1.8到JDK 21集合框架的主要演进:
- JDK 8: - 引入Stream API,支持集合的函数式操作
- 添加forEach()、removeIf()等默认方法
- HashMap使用红黑树优化哈希冲突
 
- JDK 9: - 新增不可变集合工厂方法:List.of()、Set.of()、Map.of()
- 增强Stream API,添加takeWhile()、dropWhile()等方法
 
- JDK 10: - 新增List.copyOf()、Set.copyOf()、Map.copyOf()方法
- 优化集合的序列化和反序列化
 
- JDK 16: - 引入Stream.toList()方法,返回不可变列表
- 增强Map接口,添加computeIfAbsent()等方法
 
- JDK 21: - 新增SequencedCollection接口,提供统一的首尾元素操作
- 增强LinkedList,实现SequencedCollection接口
- 新增Record集合工厂方法,如List.of(record...)等
 
演进趋势:
- 不可变集合支持增强
- 函数式编程支持增强
- 接口方法不断丰富
- 性能持续优化
- 并发集合功能增强
实际应用建议:在新项目中优先使用JDK 11及以上LTS版本,充分利用不可变集合和Stream API提升代码质量和性能。
