欢迎光临
我们一直在努力

一篇文章带你彻底搞懂 Java 集合框架(List/Set/Map)

前言

集合是 Java 开发每日必用的数据容器,不管是接口接收参数、业务数据存储、结果去重、键值映射都离不开 List、Set、Map。 但绝大多数开发者只会调用add()、get()、put(),对底层存储结构、扩容逻辑、哈希冲突、线程安全一知半解,线上频繁出现内存溢出、查询缓慢、数据重复、并发丢数据等问题。 本文搭配体系结构图、对比表格、底层源码拆解,一次性吃透 Java 集合全体系,同时覆盖面试高频考点,看完既能应对八股面试,也能规范业务开发选型。

一、集合框架完整体系梳理

Java 集合分为两大根接口:Collection单列集合、Map双列键值集合。

  • Collection(单列,存单个元素)
    • List:有序、可重复、支持索引;实现类:ArrayList、LinkedList、Vector
    • Set:无序、不可重复;实现类:HashSet、TreeSet、LinkedHashSet
    • Queue:队列,先进先出;实现类:ArrayDeque、PriorityQueue
  • Map(双列,键值对 key-value)
    • HashMap:无序键值存储,哈希表实现
    • TreeMap:按键排序
    • LinkedHashMap:插入有序 HashMap
    • HashTable:老旧线程安全 Map(不推荐使用)

    上图完整展示接口继承、实现类层级关系,区分 List/Set/Map 三大分支,是学习集合的基础,面试常让手绘该结构图。

    二、ArrayList vs LinkedList 底层结构与业务选型

    2.1 底层存储源码对比

    ArrayList

    底层是动态 Object 数组,默认初始化容量 10,存连续内存:

    java

    运行

    transient Object[] elementData; // 存储元素数组
    private int size; // 当前元素数量
    // 扩容机制:扩容至原容量1.5倍
    private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    }

    扩容流程:新增元素超出数组长度 → 创建 1.5 倍长度新数组 → System.arraycopy拷贝全部元素,频繁扩容会产生大量数组拷贝损耗性能。

    LinkedList

    底层是双向链表,无数组、无扩容概念,每个节点存前驱、后继指针:

    java

    运行

    private static class Node<E> {
    E item;
    Node<E> next; // 下一个节点
    Node<E> prev; // 上一个节点
    }

    元素分散在堆内存,无连续地址,依靠指针串联。

    2.2 核心性能对比表

    表格

    特性ArrayListLinkedList
    底层结构 动态数组 双向链表
    随机 get 查询 O (1),直接数组下标寻址 O (n),需从头遍历节点
    头部插入删除 O (n),数组全部元素后移 O (1),仅修改前后节点指针
    尾部追加 O (1)(不触发扩容) O(1)
    内存占用 紧凑连续内存,开销小 每个节点额外存两个指针,内存开销大
    遍历效率 for 循环、Iterator 效率极高 不推荐普通 for 循环,只能用迭代器

    2.3 业务选型规范

  • 90% 日常业务优先ArrayList:绝大多数场景都是尾部新增、根据索引查询;
  • 仅高频头部 / 中间插入删除、数据量超大时选用LinkedList;
  • 固定长度数据可使用Arrays.asList或手动初始化容量,减少 ArrayList 扩容次数。
  • 三、HashSet 去重原理与 equals/hashCode 强制约定

    3.1 HashSet 底层本质

    HashSet底层完全依赖HashMap实现,存入元素作为 HashMap 的 key,value 统一为静态空 Object:

    java

    运行

    public HashSet() {
    map = new HashMap<>();
    }
    public boolean add(E e) {
    return map.put(e, PRESENT)==null;
    }

    所以 HashSet 去重逻辑完全复用 HashMap 的 key 判重规则。

    3.2 去重判断两步流程

  • 调用对象hashCode()计算哈希值,定位哈希桶;
  • 若桶内无元素 → 直接存入;若桶内已有元素,调用equals()对比内容:
    • equals 返回 true:判定为重复,不存入;
    • equals 返回 false:哈希冲突,链表 / 红黑树追加节点。
  • 3.3 开发强制规范(面试必考题)

    自定义实体存入 HashSet,必须同时重写hashCode()与equals(),缺一不可:

  • 只重写 equals、不重写 hashCode:相同内容对象哈希值不同,判定为两个对象,无法去重;
  • 只重写 hashCode、不重写 equals:不同内容可能哈希值碰撞,错误判定重复;
  • 错误示例:

    java

    运行

    class User {
    String id;
    // 仅重写equals,未重写hashCode
    @Override
    public boolean equals(Object o) {
    if (this == o) return true;
    User user = (User) o;
    return Objects.equals(id, user.id);
    }
    }
    // 两个id相同的User对象,hashCode不同,HashSet会同时存储,去重失效

    四、HashMap 工作原理(全文核心重点)

    4.1 底层存储结构(JDK1.8 优化)

  • 主体:Node<K,V>[] table哈希数组(哈希桶);
  • 哈希冲突少时,桶内元素用单向链表存储;
  • 链表长度≥8 且 数组容量≥64,链表转为红黑树,查询时间复杂度从 O (n) 降至 O (logn);
  • 红黑树节点数量≤6,退化为链表。
  • 4.2 put 插入完整流程

  • 计算 key 哈希值 hash = (key.hashCode()) ^ (hash >>> 16),高低位混合减少冲突;
  • 通过 hash & (table.length – 1) 计算数组下标;
  • 判断哈希桶是否为空,为空直接新建 Node 放入;
  • 桶不为空,对比桶首节点 key:hash 相等 && (== 或 equals) → 覆盖旧 value;
  • key 不匹配,判断当前桶是红黑树还是链表:
    • 红黑树:树内查找 key,存在则覆盖,不存在新增树节点;
    • 链表:循环遍历链表,找到相同 key 覆盖,末尾追加新节点;追加后链表长度达到 8,触发树化;
  • 插入完成,size 自增,若size > 阈值(容量*0.75),触发 2 倍扩容。
  • 4.3 关键参数详解

  • 默认初始容量:16,容量永远是 2 的幂次方,方便位运算取模;
  • 负载因子 0.75:达到容量 75% 时扩容,平衡哈希冲突与内存占用;
  • 树化阈值 8、退化阈值 6;
  • 扩容规则:容量翻倍,重新计算所有元素哈希下标,元素分为高低两条链表迁移。
  • 4.4 开发避坑点

  • 预估数据量时,初始化 HashMap 指定容量:new HashMap<>(预估数量/0.75 + 1),避免多次扩容;
  • 自定义对象作为 key,必须重写 hashCode 与 equals;
  • HashMap 非线程安全,并发场景使用ConcurrentHashMap,禁止手动加锁 HashMap。
  • 五、集合遍历的 5 种方式、优缺点与使用场景

    5.1 普通 for 循环(仅 List 可用)

    java

    运行

    ArrayList<String> list = new ArrayList<>();
    for(int i = 0; i < list.size(); i++){
    String s = list.get(i);
    }

    优点:可获取索引、支持遍历中修改元素; 缺点:LinkedList 使用该方式效率极低,每次 get 都从头遍历。

    5.2 增强 for 循环(foreach,全部集合通用)

    java

    运行

    for(String s : list){
    System.out.println(s);
    }

    底层依赖迭代器 Iterator,语法简洁; 坑:遍历中调用集合add/remove会触发ConcurrentModificationException并发修改异常。

    5.3 Iterator 迭代器(通用,支持安全删除)

    java

    运行

    Iterator<String> it = list.iterator();
    while(it.hasNext()){
    String s = it.next();
    it.remove(); // 迭代器自身删除方法,不会报并发异常
    }

    适用:遍历过程中需要删除元素的场景。

    5.4 ListIterator(仅 List 独有,可前后遍历)

    java

    运行

    ListIterator<String> lit = list.listIterator();
    while(lit.hasNext()){}
    while(lit.hasPrevious()){} // 反向遍历

    适合需要双向遍历 List 的特殊业务。

    5.5 Java8 Stream 流式遍历

    java

    运行

    list.stream().forEach(System.out::println);
    // 并行流大数据处理
    list.parallelStream().filter(s->s.length()>1).collect(Collectors.toList());

    优点:支持过滤、映射、分组、并行处理,函数式编程简化代码; 缺点:复杂逻辑可读性下降,并行流注意线程安全问题。

    遍历选型总结

  • ArrayList 需要索引操作:普通 for 循环;
  • 简单遍历、无需增删:foreach 增强 for;
  • 遍历过程删除元素:Iterator 迭代器;
  • 数据过滤、转换、分组、大数据并行:Stream 流;
  • LinkedList 禁止使用普通 for 循环。
  • 六、集合高频开发总结 & 面试考点

  • List:随机查询选 ArrayList,频繁中间插入删除选 LinkedList;
  • Set 去重核心依赖 hashCode+equals,自定义实体必须两个方法一起重写;
  • HashMap 核心:数组 + 链表 + 红黑树、put 完整流程、2 倍扩容、负载因子 0.75,并发使用 ConcurrentHashMap;
  • 遍历规避并发修改异常:遍历增删元素只用 Iterator.remove ();
  • 所有集合非线程安全(Vector/HashTable 除外,性能差不推荐),多线程场景选用 JUC 并发容器。
  • 结尾

    Java 集合框架是后端面试第一高频模块,底层数组、链表、哈希、红黑树逻辑贯穿性能优化、线上故障排查。熟练掌握底层原理后,后续缓存、中间件底层哈希结构都能快速理解。

    赞(0)
    未经允许不得转载:171主机测评 » 一篇文章带你彻底搞懂 Java 集合框架(List/Set/Map)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址