从0开始学习Java+AI知识点总结-10.集合框架、stream流
它们不仅在日常开发中高频使用,也是面试中的重点内容。本文将从集合框架的核心概念出发,详细解析 Set、Map 集合的特性与底层原理,再深入 Stream 流的使用技巧,最后通过实战案例巩固所学知识,帮助快速入门。System.out.println("最受欢迎的景点是:" + maxEntry.getKey() + ",票数:" + maxEntry.getValue());List<String
作为 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 及之后:哈希表 = 数组 + 链表 + 红黑树(优化长链表查询性能)
数据存储流程
- 创建默认长度为 16、加载因子为 0.75 的数组(加载因子:数组扩容阈值比例)
- 通过元素哈希值对数组长度取模,计算存储位置
- 若位置为空,直接存入;若不为空:
- 调用equals()方法比较元素,相同则不存储(去重)
- 不同则存入链表(JDK8 前新元素占老元素位置,JDK8 后新元素挂在链表尾部)
- 当数组元素数达到16*0.75=12时,数组扩容为原长度的 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() |
去除重复元素(依赖hashCode和equals) |
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); }; } } |
六、学习总结与建议
- 底层原理是关键:理解哈希表、红黑树等数据结构的工作原理,能帮助你更好地选择合适的集合类。
- 注重场景选择:不同集合类有不同特性,根据是否需要顺序、排序、索引等需求选择最优实现类。
- 多练习 Stream 流:Stream 流能极大简化集合操作,熟练掌握中间操作和终结操作的组合使用。
- 善用工具类:Collections、Arrays 等工具类提供了大量实用方法,能减少重复代码。
Java 集合框架与 Stream 流是 Java 开发的基石,通过反复练习和实战应用,才能真正掌握这些知识点。建议结合实际场景多编写代码,逐步积累经验,为后续学习打下坚实基础。
更多推荐
所有评论(0)