JAVA数据结构与算法 - 基础:常用集合简述

12、数据结构与算法 - 基础:常用集合简述

一、Java 集合框架全景图

Java 集合框架(Java Collections Framework,JCF)是 JDK 1.2 引入的一套统一数据结构体系,它将所有常用数据容器纳入统一的接口层级,并提供了算法工具类和线程安全包装。整个框架以两大根接口为起点分叉:

dart 复制代码
                        Iterable
                           |
                    ┌──────┴──────┐
               Collection        Map
               /    |    \      /    \
            List  Queue  Set   HashMap  SortedMap
            /      |      \     /  \       |
     ArrayList  Deque  HashSet  ...  ConcurrentHashMap
     LinkedList  |     /    \
     Vector    ArrayDeque  LinkedHashSet
      |                   TreeSet
     Stack

Collection 接口代表一组对象的集合,细分为三个子接口:

  • List:有序、可重复、按索引访问
  • Set:无序、不可重复
  • Queue / Deque:按特定规则进出(FIFO / LIFO / 优先级)

Map 接口 代表键值对的映射,键不可重复,每个键映射到一个值。注意:Map 并不继承 Collection,它是独立的层级。

二、List 家族:有序可重复的线性列表

2.1 核心实现类对比

特性 ArrayList LinkedList Vector Stack
底层数据结构 动态数组(Object[]) 双向链表 动态数组 继承 Vector
随机访问(get) O(1) O(N)(需遍历) O(1) O(1)
头/尾插入 O(N) / 均摊 O(1) O(1) / O(1) O(N) / 均摊 O(1) O(N) / 均摊 O(1)
中间插入/删除 O(N)(移动元素) O(1)(修改指针,但查找 O(N)) O(N) O(N)
内存占用 仅数据 + 少量预留 数据 + 两个指针(prev/next) 同 ArrayList 同 Vector
线程安全 ✅(synchronized)
扩容策略 1.5 倍(oldCapacity + oldCapacity >> 1) 按需分配节点 2 倍(可指定增量) 同 Vector
适用场景 随机访问多、尾部增删多 频繁头尾操作、无随机访问需求 旧代码遗留,极少新用 需要 LIFO 栈结构

2.2 ArrayList 的动态扩容机制

java 复制代码
/**
 * 模拟 ArrayList 的核心扩容逻辑
 * 演示容量增长和元素拷贝过程
 */
public class ArrayListSimulator<T> {

    private Object[] elements;
    private int size;
    private static final int DEFAULT_CAPACITY = 10;

    public ArrayListSimulator() {
        elements = new Object[DEFAULT_CAPACITY];
    }

    public void add(T element) {
        ensureCapacity(size + 1);
        elements[size++] = element;
    }

    /** 扩容:新容量 = 旧容量 + 旧容量/2(即 1.5 倍) */
    private void ensureCapacity(int minCapacity) {
        if (minCapacity > elements.length) {
            int oldCapacity = elements.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);  // 1.5 倍
            if (newCapacity < minCapacity) {
                newCapacity = minCapacity;
            }
            elements = Arrays.copyOf(elements, newCapacity);
        }
    }

    @SuppressWarnings("unchecked")
    public T get(int index) {
        if (index >= size) throw new IndexOutOfBoundsException();
        return (T) elements[index];
    }

    public int size() { return size; }

    // 测试
    public static void main(String[] args) {
        ArrayListSimulator<String> list = new ArrayListSimulator<>();
        for (int i = 0; i < 25; i++) {
            list.add("元素" + i);
        }
        System.out.println("添加25个元素后 size = " + list.size());
        System.out.println("索引0: " + list.get(0));
        System.out.println("索引24: " + list.get(24));

        // 边界测试
        try {
            list.get(99);
        } catch (IndexOutOfBoundsException e) {
            System.out.println("越界异常捕获成功");
        }
    }
}

2.3 LinkedList 的双向链表特性

LinkedList 同时实现了 ListDeque 两个接口,这意味着它既是一个可索引的列表,又是一个双端队列。底层是带哨兵节点的双向链表,头部和尾部操作均为 O(1)。但由于没有维护索引结构,get(index) 需要从头部(或尾部,JDK 会选较近的一端)遍历,实际为 O(N)。

三、Set 家族:不可重复的无序/有序集合

3.1 核心实现类对比

特性 HashSet LinkedHashSet TreeSet
底层实现 HashMap(键存元素,值存 dummy Object) LinkedHashMap TreeMap(红黑树)
元素顺序 无序(取决于哈希分布) 插入顺序(链表维护) 自然排序或 Comparator 排序
查找/插入/删除 O(1) 平均 O(1) 平均 O(log N)
null 元素 ✅ 允许一个 ✅ 允许一个 ❌ 不允许(需要比较)
内存开销 中等(HashMap 内部结构) 较高(额外链表指针) 较高(树节点 + 颜色标记)
适用场景 通用去重 需要保持插入顺序的去重 需要有序遍历的去重

3.2 HashSet 去重原理剖析

HashSet 的去重依赖于两个方法:hashCode()equals()。当添加一个元素时,流程如下:

  1. 计算元素的 hashCode(),定位到对应的哈希桶
  2. 若桶为空,直接插入成功
  3. 若桶非空,遍历桶内元素,调用 equals() 逐一比较
  4. 若有 equals() 返回 true 的元素,视为重复,添加失败
  5. 若无匹配,追加到桶链表(或红黑树节点)中

重写 equals 必须同时重写 hashCode ,这是 Java 对象相等性的"契约"。两个对象若 equals 为 true,其 hashCode 必须相同;反之则不一定。违反此约定会导致 HashSet/HashMap 出现"幽灵重复"------两个逻辑相等的对象被视为不同。

java 复制代码
import java.util.*;

/**
 * HashSet 去重原理演示
 * 包含正确重写 equals/hashCode 的示例类
 */
public class HashSetDedupDemo {

    /** 正确重写了 equals 和 hashCode 的学生类 */
    static class Student {
        int id;
        String name;

        Student(int id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Student)) return false;
            Student other = (Student) o;
            return id == other.id;  // 以 id 判断是否同一学生
        }

        @Override
        public int hashCode() {
            return Objects.hash(id);  // 与 equals 保持一致
        }

        @Override
        public String toString() {
            return "Student{id=" + id + ", name='" + name + "'}";
        }
    }

    /** 错误示范:只重写 equals,没有重写 hashCode */
    static class BadStudent {
        int id;
        BadStudent(int id) { this.id = id; }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof BadStudent)) return false;
            return id == ((BadStudent) o).id;
        }
        // 未重写 hashCode → 默认使用 Object 的 hashCode(基于内存地址)
    }

    public static void main(String[] args) {
        // 正确示例
        Set<Student> set1 = new HashSet<>();
        set1.add(new Student(1, "张三"));
        set1.add(new Student(1, "张三改"));  // 同 id,视为重复
        set1.add(new Student(2, "李四"));
        System.out.println("正确去重: " + set1);
        // 输出: [Student{id=1, name='张三'}, Student{id=2, name='李四'}]
        // 注意:第二个 id=1 的元素被拒绝

        // 错误示例:未重写 hashCode 导致去重失败
        Set<BadStudent> set2 = new HashSet<>();
        set2.add(new BadStudent(100));
        set2.add(new BadStudent(100));  // 本应去重,但因 hashCode 不同而重复添加
        System.out.println("错误示范(set大小应为1): " + set2.size());  // 输出 2

        // LinkedHashSet:保持插入顺序
        Set<String> linkedSet = new LinkedHashSet<>();
        linkedSet.add("Cherry");
        linkedSet.add("Banana");
        linkedSet.add("Apple");
        System.out.println("LinkedHashSet(插入顺序): " + linkedSet);
        // [Cherry, Banana, Apple]

        // TreeSet:自然排序
        Set<String> treeSet = new TreeSet<>();
        treeSet.add("Cherry");
        treeSet.add("Banana");
        treeSet.add("Apple");
        System.out.println("TreeSet(字典序): " + treeSet);
        // [Apple, Banana, Cherry]
    }
}

四、Map 家族:键值对的统治

4.1 核心实现类全方位对比

特性 HashMap LinkedHashMap TreeMap Hashtable ConcurrentHashMap
底层结构 数组 + 链表 + 红黑树 HashMap + 双向链表 红黑树 数组 + 链表 Node 数组 + CAS + synchronized
键值顺序 无序 插入顺序 / 访问顺序 键自然排序或 Comparator 无序 无序
null 键/值 ✅ 允许 ✅ 允许 ❌ 键不允许 / ✅ 值允许 ❌ 均不允许 ❌ 均不允许
线程安全 ✅(方法级锁) ✅(细粒度锁,高并发)
默认容量 16 16 --- 11 16
装载因子 0.75 0.75 --- 0.75 0.75
扩容倍数 --- 2×+1
JDK 版本 1.2 1.4 1.2 1.0(遗留) 1.5(JUC)
推荐使用 ✅(需顺序时) ✅(需排序时) ❌ 不推荐 ✅(并发场景)

4.2 TreeMap 的红黑树排序之旅

java 复制代码
import java.util.*;

/**
 * TreeMap 排序演示
 * 展示自然排序(Comparable)与自定义排序(Comparator)
 */
public class TreeMapSortDemo {

    /** 自定义比较器:按字符串长度排序,等长按字母序 */
    static class LengthComparator implements Comparator<String> {
        @Override
        public int compare(String s1, String s2) {
            int lenDiff = s1.length() - s2.length();
            return lenDiff != 0 ? lenDiff : s1.compareTo(s2);
        }
    }

    public static void main(String[] args) {
        // 默认自然排序(字典序)
        TreeMap<String, Integer> defaultMap = new TreeMap<>();
        defaultMap.put("dog", 1);
        defaultMap.put("apple", 2);
        defaultMap.put("cat", 3);
        defaultMap.put("banana", 4);
        System.out.println("自然排序: " + defaultMap.keySet());
        // [apple, banana, cat, dog]

        // 自定义排序(按长度)
        TreeMap<String, Integer> customMap = new TreeMap<>(new LengthComparator());
        customMap.put("dog", 1);
        customMap.put("apple", 2);
        customMap.put("cat", 3);
        customMap.put("banana", 4);
        System.out.println("按长度排序: " + customMap.keySet());
        // [cat, dog, apple, banana]  ------ 注意等长的 dog/cat 按字母序

        // 获取子映射
        System.out.println("headMap('dog'): " + defaultMap.headMap("dog"));
        // {apple=2, banana=4, cat=3}
        System.out.println("tailMap('dog'): " + defaultMap.tailMap("dog"));
        // {dog=1}

        // 最值操作
        System.out.println("firstKey: " + defaultMap.firstKey());     // apple
        System.out.println("lastKey: " + defaultMap.lastKey());       // dog

        // 边界测试:空 TreeMap
        TreeMap<String, Integer> emptyMap = new TreeMap<>();
        try {
            emptyMap.firstKey();
        } catch (NoSuchElementException e) {
            System.out.println("空TreeMap获取firstKey抛异常(预期行为)");
        }
    }
}

五、Queue / Deque 家族:先进先出与双端操作

特性 LinkedList(作队列用) ArrayDeque PriorityQueue
底层 双向链表 循环数组 二叉堆(小顶堆)
FIFO 操作 O(1) O(1) 均摊 O(log N)(出队)
双向操作 ✅ Deque ✅ Deque ❌ 仅单向
null 元素
线程安全
适用场景 频繁头尾操作 高性能栈/队列(推荐替代 Stack 和 LinkedList) 优先队列(任务调度、Top K)

最佳实践:用 ArrayDeque 替代 Stack(Stack 已过时)和用作 FIFO 队列的 LinkedList(ArrayDeque 更省内存且更快,因为无链表指针开销且缓存友好)。

六、线程安全方案选择指南

方案 原理 性能 适用场景
Vector / Hashtable synchronized 方法锁 低(竞争时严重退化) 不推荐(遗留类)
Collections.synchronizedXXX() 装饰器模式,全表锁 低并发、简单场景
ConcurrentHashMap JDK 7 分段锁 → JDK 8 CAS+synchronized 高(细粒度,非阻塞读) 高并发读写
CopyOnWriteArrayList 写时复制数组 读极快,写很慢 读多写极少(如监听器列表)
ConcurrentLinkedQueue CAS 无锁队列 高并发生产者-消费者
BlockingQueue(Array/Linked) ReentrantLock + Condition 生产者-消费者,线程池任务队列

七、集合选用速查表

按场景需求快速定位合适的集合类:

你的需求 推荐集合 理由
简单存储,随机访问多 ArrayList O(1) 索引
频繁头尾增删 LinkedListArrayDeque O(1) 头尾操作
需要栈(LIFO) ArrayDeque 比 Stack 快且现代
需要队列(FIFO) ArrayDequeLinkedList 两者均实现了 Queue
去重,不关心顺序 HashSet O(1) 增删查
去重,保持插入顺序 LinkedHashSet O(1) + 有序遍历
去重,需要排序 TreeSet O(log N) + 有序
键值映射,高性能 HashMap O(1) 平均
键值映射,需顺序 LinkedHashMap O(1) + 有序遍历(可实现 LRU)
键值映射,需排序 TreeMap O(log N) + 有序键
高并发键值映射 ConcurrentHashMap 线程安全 + 高性能
读多写少的 List CopyOnWriteArrayList 无锁读
优先级处理 PriorityQueue 二叉堆,O(log N)
线程安全的生产消费 ArrayBlockingQueue / LinkedBlockingQueue 阻塞队列

八、集合框架的迭代器与 fail-fast 机制

Java 集合的迭代器(Iterator)在遍历时会检测结构性修改(非迭代器自身的 add/remove 操作)。若检测到并发修改(modCount 变化),会立即抛出 ConcurrentModificationException,这种策略称为 fail-fast(快速失败)。

java 复制代码
import java.util.*;

/**
 * fail-fast 机制演示
 */
public class FailFastDemo {

    public static void main(String[] args) {
        // 正面示例:使用迭代器的 remove 安全删除
        List<String> list1 = new ArrayList<>(
            Arrays.asList("A", "B", "C", "D"));
        Iterator<String> it1 = list1.iterator();
        while (it1.hasNext()) {
            String s = it1.next();
            if ("B".equals(s)) {
                it1.remove();  // 迭代器自身的 remove,安全
            }
        }
        System.out.println("安全删除后: " + list1);  // [A, C, D]

        // 反面示例:遍历中直接修改集合 → ConcurrentModificationException
        List<String> list2 = new ArrayList<>(
            Arrays.asList("X", "Y", "Z"));
        try {
            for (String s : list2) {  // for-each 底层也是迭代器
                if ("Y".equals(s)) {
                    list2.remove(s);  // 直接修改集合,触发 fail-fast
                }
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("捕获到 ConcurrentModificationException(预期)");
        }

        // 正确替代方案 1:使用 removeIf(Java 8+)
        List<String> list3 = new ArrayList<>(
            Arrays.asList("X", "Y", "Z"));
        list3.removeIf(s -> "Y".equals(s));
        System.out.println("removeIf 后: " + list3);  // [X, Z]

        // 正确替代方案 2:使用 CopyOnWriteArrayList(无 fail-fast)
        CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>(
            Arrays.asList("1", "2", "3"));
        for (String s : cowList) {
            if ("2".equals(s)) {
                cowList.remove(s);  // 不会抛异常,但效率低
            }
        }
        System.out.println("CopyOnWriteArrayList 删除后: " + cowList);  // [1, 3]
    }
}

九、Comparable 与 Comparator:排序的两种姿势

特性 Comparable Comparator
所属包 java.lang java.util
实现方法 compareTo(T o) compare(T o1, T o2)
实现位置 在待比较的类内部实现 在外部独立定义
耦合度 高(侵入类定义) 低(解耦,策略模式)
排序方式 单一(自然顺序) 多种(可定义多个比较器)
典型用法 String、Integer 等 JDK 内置类 第三方类或多种排序需求
调用方式 Collections.sort(list) Collections.sort(list, comparator)

优先使用 Comparator ------ 更灵活、低耦合,符合开闭原则。数据类本身不应该承担排序策略。

十、总结

Java 集合框架是数据结构理论在工业级编程语言中的集大成体现:

  • List 解决有序存储问题,Array 和 Linked 两种底层实现覆盖了随机访问与频繁修改两大类场景
  • Set 解决去重问题,Hash / Linked / Tree 三剑客各司其职
  • Map 解决键值映射问题,从 HashMap 的无序高性能到 TreeMap 的有序慢速,再到 ConcurrentHashMap 的并发高性能,层次分明
  • Queue/Deque 解决排队问题,ArrayDeque 已成为 Stack 和 LinkedList 队列的最佳替代

选择集合类时遵循三问原则:(1) 是否需要线程安全?(2) 是否关心元素顺序?(3) 读写频率如何?回答这三个问题,就能从框架全景图中精准定位最合适的实现。

相关推荐
噢,我明白了1 小时前
MyBatis-Plus的引入和配置
java·tomcat·mybatis
TYKJ0231 小时前
带宽100M但传输只有30M?你的服务器可能该换TCP算法了
后端·算法
SilentSamsara1 小时前
运算符重载:让自定义对象支持 +、[]、in 操作
开发语言·python·算法·青少年编程·pycharm
日月云棠1 小时前
JAVA数据结构与算法 - 基础:BlockingQueue
java·算法
哪吒编程1 小时前
GPT 5.5 Thinking深度思考了十几分钟,给我挖了一个排查一周的并发大坑
java
likerhood1 小时前
设计模式 · 享元模式(Flyweight Pattern)java
java·设计模式·享元模式
Royzst2 小时前
图书管理案例
java·开发语言
8K超高清2 小时前
CCBN展会多图回顾
人工智能·算法·fpga开发·接口隔离原则·智能硬件
带刺的坐椅2 小时前
SolonCode v2026.5.21 发布,Web 能看项目,IM 能找队友
java·ai编程·数字员工·soloncode·终端智能体