作为 Java 开发的核心基础,集合框架与 Stream 流是每个 Java 初学者必须掌握的知识点。它们不仅在日常开发中高频使用,也是面试中的重点内容。本文将从集合框架的核心概念出发,详细解析 Set、Map 集合的特性与底层原理,再深入 Stream 流的使用技巧,最后通过实战案例巩固所学知识,帮助快速入门。

一、集合框架概述

Java 集合框架是用于存储和操作一组对象的容器,主要分为单列集合(Collection) 和双列集合(Map) 两大类。其中 Collection 接口下又分为 List 和 Set 两大分支,它们的核心区别在于:

  • List 集合:有序、可重复、有索引(如 ArrayList、LinkedList)
  • Set 集合:无序 / 有序、不重复、无索引(如 HashSet、TreeSet)

集合框架的优势在于:

  • 提供统一的操作接口,降低使用成本
  • 封装了数据结构的实现细节,开发者无需关注底层
  • 支持动态扩容,无需手动管理容量
  • 提供丰富的操作方法(增删改查、排序、过滤等)

二、Set 集合详解

Set 集合是 Collection 的重要分支,核心特点是不重复、无索引,部分实现类支持有序性。常用实现类包括 HashSet、LinkedHashSet 和 TreeSet。

2.1 Set 集合核心特点

实现类

核心特点

底层结构

适用场景

HashSet

无序、不重复、无索引

哈希表(数组 + 链表 + 红黑树)

无需保证顺序,追求高效增删改查

LinkedHashSet

有序、不重复、无索引

哈希表 + 双链表

需要记录添加顺序的去重场景

TreeSet

可排序、不重复、无索引

红黑树

需要对元素排序的场景

注意:Set 集合的常用方法均继承自 Collection 接口,自身几乎没有新增方法。

2.2 HashSet 底层原理

HashSet 是最常用的 Set 实现类,其核心特性由底层哈希表(Hash Table)决定。

哈希值基础
  • 哈希值是对象通过hashCode()方法生成的 int 类型随机值,每个对象都有唯一哈希值(但可能存在哈希碰撞)
  • 同一对象多次调用hashCode()返回值相同
  • 不同对象可能生成相同哈希值(哈希碰撞),需通过equals()方法进一步判断
哈希表结构
  • JDK8 之前:哈希表 = 数组 + 链表
  • JDK8 及之后:哈希表 = 数组 + 链表 + 红黑树(优化长链表查询性能)
数据存储流程
  1. 创建默认长度为 16、加载因子为 0.75 的数组(加载因子:数组扩容阈值比例)
  2. 通过元素哈希值对数组长度取模,计算存储位置
  3. 若位置为空,直接存入;若不为空:
    • 调用equals()方法比较元素,相同则不存储(去重)
    • 不同则存入链表(JDK8 前新元素占老元素位置,JDK8 后新元素挂在链表尾部)
  1. 当数组元素数达到16*0.75=12时,数组扩容为原长度的 2 倍
  2. 当链表长度超过 8 且数组长度≥64 时,链表自动转为红黑树
去重机制

要实现自定义对象在 HashSet 中的去重,必须重写类的hashCode()equals()方法:

class Student {

    private String name;

    private int age;

    // 重写hashCode和equals,确保属性相同的对象被视为重复

    @Override

    public int hashCode() {

        return Objects.hash(name, age);

    }

    @Override

    public boolean equals(Object o) {

        if (this == o) return true;

        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        return age == student.age && Objects.equals(name, student.name);

    }

}

2.3 LinkedHashSet 底层原理

LinkedHashSet 在 HashSet 基础上增加了双链表机制,用于记录元素的添加顺序,因此具备 "有序" 特性:

  • 底层仍基于哈希表(数组 + 链表 + 红黑树)实现去重和高效操作
  • 额外通过双链表维护元素的插入顺序,遍历顺序与添加顺序一致
  • 性能略低于 HashSet(维护链表带来额外开销),但保留了顺序特性

2.4 TreeSet 排序机制

TreeSet 的核心特性是可排序,底层基于红黑树(一种自平衡二叉树)实现,支持两种排序方式:

自然排序(Comparable 接口)

让自定义类实现Comparable接口,重写compareTo()方法:

class Student implements Comparable<Student> {

    private String name;

    private int age;

    // 按年龄升序排序,年龄相同则按姓名排序

    @Override

    public int compareTo(Student o) {

        int ageDiff = this.age - o.age;

        return ageDiff != 0 ? ageDiff : this.name.compareTo(o.name);

    }

}

定制排序(Comparator 比较器)

通过 TreeSet 的有参构造器指定排序规则:

// 创建按年龄降序排序的TreeSet

Set<Student> set = new TreeSet<>((s1, s2) -> s2.age - s1.age);

排序规则说明
  • 返回正整数:当前元素 > 比较元素(放右侧)
  • 返回负整数:当前元素 < 比较元素(放左侧)
  • 返回 0:元素重复(不存储)

三、Map 集合详解

Map 集合是双列集合的核心,用于存储键值对(key-value) 数据,键唯一且与值一一对应,值可重复。

3.1 Map 集合核心概念

  • 格式:{key1=value1, key2=value2, ...}
  • 键(key):不重复、无索引,决定集合特性
  • 值(value):可重复,无索引
  • 适用场景:需要存储一一对应关系的数据(如购物车、投票统计)

常用实现类及特点:

实现类

核心特点

底层结构

适用场景

HashMap

键无序、不重复、无索引

哈希表(数组 + 链表 + 红黑树)

无需保证顺序,追求高效操作

LinkedHashMap

键有序、不重复、无索引

哈希表 + 双链表

需要记录键添加顺序的场景

TreeMap

键可排序、不重复、无索引

红黑树

需要对键排序的场景

3.2 Map 集合常用方法

Map 接口提供了丰富的操作方法,核心方法如下:

方法名称

说明

V put(K key, V value)

添加键值对(键存在则覆盖值)

V get(Object key)

根据键获取值

V remove(Object key)

根据键删除键值对

int size()

获取集合大小

boolean isEmpty()

判断集合是否为空

boolean containsKey(K key)

判断是否包含指定键

boolean containsValue(V value)

判断是否包含指定值

Set<K> keySet()

获取所有键的 Set 集合

Collection<V> values()

获取所有值的 Collection 集合

Set<Map.Entry<K,V>> entrySet()

获取所有键值对的 Set 集合

3.3 Map 集合遍历方式

Map 集合有三种常用遍历方式,适用于不同场景:

方式 1:键找值(最常用)

通过keySet()获取所有键,再遍历键获取对应值:

Map<String, Integer> map = new HashMap<>();

map.put("苹果", 5);

map.put("香蕉", 3);

// 遍历所有键

for (String key : map.keySet()) {

    Integer value = map.get(key);

    System.out.println(key + "=" + value);

}

方式 2:键值对遍历

通过entrySet()获取键值对对象,直接获取键和值:

for (Map.Entry<String, Integer> entry : map.entrySet()) {

    String key = entry.getKey();

    Integer value = entry.getValue();

    System.out.println(key + "=" + value);

}

方式 3:Lambda 表达式(JDK8+)

通过forEach()方法结合 Lambda 简化遍历:

map.forEach((key, value) -> System.out.println(key + "=" + value));

3.4 实现类底层原理

HashMap 底层原理

与 HashSet 底层原理完全一致(HashSet 本质是 HashMap 的键集合):

  • JDK8 前:数组 + 链表
  • JDK8 后:数组 + 链表 + 红黑树(链表长度 > 8 且数组≥64 时转红黑树)
  • 初始容量 16,加载因子 0.75,扩容为原 2 倍
LinkedHashMap 底层原理

在 HashMap 基础上增加双链表记录键的添加顺序,因此遍历顺序与插入顺序一致。

TreeMap 底层原理

与 TreeSet 类似,基于红黑树实现键的排序,支持两种排序方式:

  • 自然排序:键实现Comparable接口
  • 定制排序:通过构造器传入Comparator比较器

四、Stream 流详解

Stream 流是 JDK8 新增的 API,用于简化集合 / 数组的操作,结合 Lambda 表达式让代码更简洁高效。

4.1 Stream 流核心概念

  • 本质:数据处理流水线,对数据源(集合 / 数组)进行一系列操作
  • 特点:
    • 不存储数据,仅处理数据
    • 操作不会改变原数据源
    • 延迟执行:中间操作仅在终结操作调用时才执行
  • 优势:代码简洁、可读性高、支持链式编程

4.2 Stream 流使用步骤

Stream 流操作分为三步:获取流 → 中间操作 → 终结操作

步骤 1:获取 Stream 流

数据源类型

获取方式

示例代码

集合

通过stream()方法

List<String> list = new ArrayList<>(); Stream<String> stream = list.stream();

数组

通过Arrays.stream()方法

String[] arr = {"a", "b"}; Stream<String> stream = Arrays.stream(arr);

零散数据

通过Stream.of()方法

Stream<String> stream = Stream.of("a", "b", "c");

步骤 2:中间操作(返回新 Stream,支持链式调用)

常用中间操作:

方法名称

说明

filter(Predicate)

过滤符合条件的元素

sorted()

自然排序

sorted(Comparator)

定制排序

limit(long)

获取前 n 个元素

skip(long)

跳过前 n 个元素

distinct()

去除重复元素(依赖hashCodeequals

map(Function)

元素转换(如 String 转 Integer)

Stream.concat(a,b)

合并两个流

示例:

List<String> list = Arrays.asList("张三", "李四", "张三丰", "张无忌");

// 过滤姓张且长度为3的元素

Stream<String> stream = list.stream()

    .filter(name -> name.startsWith("张"))  // 过滤姓张

    .filter(name -> name.length() == 3);   // 过滤长度为3

步骤 3:终结操作(触发执行,返回非 Stream 结果)

常用终结操作:

方法名称

说明

forEach(Consumer)

遍历元素

count()

统计元素个数

max(Comparator)

获取最大值

min(Comparator)

获取最小值

collect(Collector)

收集流到集合 / 数组

示例:

// 遍历结果

stream.forEach(System.out::println);  // 输出:张三丰 张无忌

// 收集到List集合

List<String> result = list.stream()

    .filter(name -> name.startsWith("张"))

    .collect(Collectors.toList());

4.3 收集 Stream 流

通过collect()方法将流处理结果收集到集合或数组:

// 收集到List

List<String> list = stream.collect(Collectors.toList());

// 收集到Set(自动去重)

Set<String> set = stream.collect(Collectors.toSet());

// 收集到Map(需指定键和值的生成规则)

Map<String, Integer> map = list.stream()

    .collect(Collectors.toMap(

        name -> name,          // 键:姓名

        name -> name.length()  // 值:姓名长度

    ));

五、综合实战案例

案例 1:投票统计系统

需求:统计 80 名学生对 4 个景点的投票结果,找出最受欢迎的景点。

public class Vote统计 {

    public static void main(String[] args) {

        // 1. 模拟80名学生的投票数据

        String[] spots = {"A", "B", "C", "D"};

        List<String> votes = new ArrayList<>();

        Random r = new Random();

        for (int i = 0; i < 80; i++) {

            votes.add(spots[r.nextInt(4)]);

        }

        // 2. 统计每个景点的投票数

        Map<String, Integer> countMap = new HashMap<>();

        for (String spot : votes) {

            countMap.put(spot, countMap.getOrDefault(spot, 0) + 1);

        }

        // 3. 找出投票最多的景点(使用Stream流)

        Map.Entry<String, Integer> maxEntry = countMap.entrySet().stream()

                .max(Map.Entry.comparingByValue())

                .get();

        System.out.println("最受欢迎的景点是:" + maxEntry.getKey() + ",票数:" + maxEntry.getValue());

    }

}

案例 2:斗地主游戏核心逻辑

需求:实现斗地主的洗牌、发牌、排序功能。

public class 斗地主 {

    public static void main(String[] args) {

        // 1. 准备54张牌

        String[] colors = {"♠", "♥", "♣", "♦"};

        String[] numbers = {"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", "2"};

        List<String> poker = new ArrayList<>();

        // 添加普通牌

        for (String color : colors) {

            for (String number : numbers) {

                poker.add(color + number);

            }

        }

        // 添加大小王

        poker.add("��");

        poker.add("��");

        // 2. 洗牌(使用Collections工具类)

        Collections.shuffle(poker);

        // 3. 发牌(3个玩家+底牌)

        List<String> player1 = new ArrayList<>();

        List<String> player2 = new ArrayList<>();

        List<String> player3 = new ArrayList<>();

        List<String> bottom = new ArrayList<>();

        for (int i = 0; i < poker.size(); i++) {

            String card = poker.get(i);

            if (i < 3) {

                bottom.add(card);  // 前3张为底牌

            } else if (i % 3 == 0) {

                player1.add(card);

            } else if (i % 3 == 1) {

                player2.add(card);

            } else {

                player3.add(card);

            }

        }

        // 4. 排序(自定义牌面大小规则)

        Collections.sort(player1, getCardComparator());

        Collections.sort(player2, getCardComparator());

        Collections.sort(player3, getCardComparator());

        // 5. 打印结果

        System.out.println("底牌:" + bottom);

        System.out.println("玩家1:" + player1);

        System.out.println("玩家2:" + player2);

        System.out.println("玩家3:" + player3);

    }

    // 自定义牌面排序规则

    private static Comparator<String> getCardComparator() {

        return (c1, c2) -> {

            String[] order = {"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", "2", "��", "��"};

            Map<String, Integer> rankMap = new HashMap<>();

            for (int i = 0; i < order.length; i++) {

                rankMap.put(order[i], i);

            }

            // 提取牌面数字(忽略花色)

            String num1 = c1.length() > 2 ? c1.substring(1) : c1;

            String num2 = c2.length() > 2 ? c2.substring(1) : c2;

            return rankMap.get(num1) - rankMap.get(num2);

        };

    }

}

六、学习总结与建议

  1. 底层原理是关键:理解哈希表、红黑树等数据结构的工作原理,能帮助你更好地选择合适的集合类。
  2. 注重场景选择:不同集合类有不同特性,根据是否需要顺序、排序、索引等需求选择最优实现类。
  3. 多练习 Stream 流:Stream 流能极大简化集合操作,熟练掌握中间操作和终结操作的组合使用。
  4. 善用工具类:Collections、Arrays 等工具类提供了大量实用方法,能减少重复代码。

Java 集合框架与 Stream 流是 Java 开发的基石,通过反复练习和实战应用,才能真正掌握这些知识点。建议结合实际场景多编写代码,逐步积累经验,为后续学习打下坚实基础。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐