引言
想象一下,你是一位咖啡师,面对一堆咖啡豆(集合数据),传统的处理方式是用勺子一颗颗筛选、研磨(for
循环),而 Stream 就像是一台智能咖啡机 —— 你只需按下按钮(声明操作),机器就会自动完成筛选、研磨、冲泡的流水线工作。
一、Stream 核心概念与基本操作
1. 什么是 Stream?
- 流式抽象:把集合 / 数组比作 “咖啡豆生产线”,Stream 是这条产线的 “流程规划图”。
- 链式操作:支持
filter(筛选).map(研磨).collect(装杯)
这样的流水线操作。 - 惰性计算:中间操作(如筛选、研磨)不会立即执行,直到终止操作(如装杯、打印)触发。
2. 核心操作分类
类型 | 作用 | 示例 |
---|---|---|
中间操作 | 规划流程(不触发执行) | filter (筛选)、map (转换) |
终止操作 | 触发流程并产出结果 | collect (收集)、forEach (遍历) |
二、10 个基础技巧实战
技巧 1:过滤空元素(filter
)—— 筛选合格咖啡豆
场景:咖啡工厂剔除石头(null
)和坏豆(空字符串),只保留优质豆子。
// 模拟包含杂质的咖啡豆列表(含空字符串和null)
List<String> coffeeBeans = Arrays.asList("优质咖啡豆", "", null, "精品咖啡豆");
// 筛选非空且非null的咖啡豆:两步过滤(先判null,再判空字符串)
List<String> qualifiedBeans = coffeeBeans.stream()
.filter(bean -> bean != null) // 中间操作:剔除null(石头)
.filter(bean -> !bean.isEmpty()) // 中间操作:剔除空字符串(坏豆)
.collect(Collectors.toList()); // 终止操作:收集结果到列表
// 输出:[优质咖啡豆, 精品咖啡豆]
解读:filter
像咖啡筛网,让杂质(null/空值
)自动 “溜走”,只留精华。
技巧 2:提取对象属性(map
)—— 给咖啡豆贴产地标签
场景:从咖啡豆对象中提取产地信息,制作风味标签。
// 定义咖啡豆类(包含产地属性)
class CoffeeBean {
private String origin; // 产地(如“埃塞俄比亚”)
public CoffeeBean(String origin) {
this.origin = origin;
}
public String getOrigin() { // 获取产地的方法
return origin;
}
}
// 模拟不同产地的咖啡豆列表
List<CoffeeBean> beans = Arrays.asList(
new CoffeeBean("埃塞俄比亚"),
new CoffeeBean("哥伦比亚")
);
// 提取产地:将CoffeeBean对象“映射”为产地字符串
List<String> origins = beans.stream()
.map(CoffeeBean::getOrigin) // 中间操作:调用getOrigin()提取属性(方法引用简化Lambda)
.collect(Collectors.toList()); // 终止操作:收集所有产地到列表
// 输出:[埃塞俄比亚, 哥伦比亚]
解读:map
像 “标签打印机”,给每颗豆子印上产地,从此告别 “无名咖啡”。
技巧 3:统计数据(reduce
)—— 计算咖啡店周销量
场景:统计一周每天的拿铁销量(10, 15, 20, 18, 25, 30, 22),求总销量。
// 模拟每日销量列表
List<Integer> dailySales = Arrays.asList(10, 15, 20, 18, 25, 30, 22);
// 方式一:使用reduce累加(初始值0,累加操作Integer::sum)
int totalSales = dailySales.stream()
.reduce(0, Integer::sum); // 等价于 (acc, num) -> acc + num,初始acc=0
// 方式二:使用IntStream(避免装箱拆箱,更高效)
int totalSalesEfficient = dailySales.stream()
.mapToInt(Integer::intValue) // 中间操作:转换为原始类型流(IntStream)
.sum(); // 终止操作:直接调用sum()方法
// 两种方式输出均为:140(杯)
解读:reduce
像 “自动算账机”,把每天的销量 “揉” 成总销量,再也不用手动加加减减。
技巧 4:分组聚合(groupingBy
)—— 按风味分类咖啡豆
场景:将不同风味(酸、甜、苦)的咖啡豆分组存放,方便管理。
// 定义风味枚举
enum Flavor { ACID(酸), SWEET(甜), BITTER(苦) }
// 定义咖啡类(包含风味属性)
class Coffee {
private Flavor flavor; // 风味
public Coffee(Flavor flavor) {
this.flavor = flavor;
}
public Flavor getFlavor() { // 获取风味的方法
return flavor;
}
}
// 模拟不同风味的咖啡列表
List<Coffee> coffees = Arrays.asList(
new Coffee(Flavor.ACID), // 酸咖啡
new Coffee(Flavor.SWEET), // 甜咖啡
new Coffee(Flavor.ACID) // 酸咖啡
);
// 按风味分组:生成Map<风味, 该风味的咖啡列表>
Map<Flavor, List<Coffee>> groupedByFlavor = coffees.stream()
.collect(Collectors.groupingBy(
Coffee::getFlavor // 分组键:风味(通过getFlavor()提取)
));
// 输出:{ACID=[酸咖啡, 酸咖啡], SWEET=[甜咖啡]}
解读:groupingBy
像 “智能货架”,自动把咖啡按风味归类,找豆子时一目了然。
技巧 5:去重(distinct
)—— 剔除重复订单
场景:咖啡店处理订单时,去除重复提交的相同饮品(如用户误点两次 “拿铁”)。
// 模拟含重复订单的列表
List<String> orders = Arrays.asList("拿铁", "卡布奇诺", "拿铁", "美式");
// 去重:基于元素equals()和hashCode()自动去重
List<String> uniqueOrders = orders.stream()
.distinct() // 中间操作:去除重复元素(仅保留首次出现的)
.collect(Collectors.toList()); // 终止操作:收集结果
// 输出:[拿铁, 卡布奇诺, 美式]
解读:distinct
像 “防重复神器”,让重复订单 “消失”,避免浪费原材料。
技巧 6:排序(sorted
)—— 按价格升序排列饮品菜单
场景:咖啡店想把饮品按价格从低到高排列,方便顾客选择性价比高的选项。
// 定义饮品类(包含名称和价格)
class Drink {
private String name; // 名称(如“美式”)
private double price; // 价格(如18.0元)
public Drink(String name, double price) {
this.name = name;
this.price = price;
}
public double getPrice() { // 获取价格的方法
return price;
}
@Override
public String toString() { // 重写toString方便打印
return name + ": ¥" + price;
}
}
// 模拟饮品列表(价格无序)
List<Drink> menu = Arrays.asList(
new Drink("手冲", 38.0),
new Drink("拿铁", 25.0),
new Drink("美式", 18.0)
);
// 按价格升序排序:使用Comparator.comparingDouble提取价格并排序
List<Drink> sortedMenu = menu.stream()
.sorted(Comparator.comparingDouble(Drink::getPrice)) // 中间操作:自定义比较器(按价格)
.collect(Collectors.toList()); // 终止操作:收集结果
// 输出:
// 美式: ¥18.0
// 拿铁: ¥25.0
// 手冲: ¥38.0
解读:sorted
像 “菜单设计师”,让饮品按价格 “排排坐”,顾客一眼就能找到 “省钱选项”。
技巧 7:批量操作(forEach
)—— 给会员发送生日优惠券
场景:咖啡店给所有会员发送生日优惠券,无需手动逐个通知。
// 定义会员类(包含姓名,含发送优惠券的方法)
class Member {
private String name; // 会员姓名
public Member(String name) {
this.name = name;
}
public void sendBirthdayCoupon() { // 发送优惠券的方法
System.out.println("给 " + name + " 发送5元无门槛优惠券!");
}
}
// 模拟会员列表
List<Member> members = Arrays.asList(
new Member("张三"),
new Member("李四")
);
// 遍历并发送优惠券:对每个会员调用sendBirthdayCoupon()
members.stream()
.forEach(Member::sendBirthdayCoupon); // 终止操作:执行副作用操作(打印日志)
// 输出:
// 给 张三 发送5元无门槛优惠券!
// 给 李四 发送5元无门槛优惠券!
解读:forEach
像 “自动发券机”,一键通知所有会员,告别 “手动群发” 的繁琐。
技巧 8:转换为特定集合(toCollection
)—— 把咖啡粉装进不同容器
场景:咖啡师需要将研磨好的咖啡粉分别装入有序的玻璃瓶(LinkedList
)和去重的密封袋(HashSet
)。
// 模拟咖啡粉列表(可能有重复)
List<String> coffeePowder = Arrays.asList("埃塞俄比亚", "哥伦比亚", "埃塞俄比亚");
// 装入玻璃瓶(有序,保留插入顺序)
LinkedList<String> glassBottle = coffeePowder.stream()
.collect(Collectors.toCollection(LinkedList::new)); // 指定收集为LinkedList
// 装入密封袋(无序且去重)
Set<String> sealedBag = coffeePowder.stream()
.collect(Collectors.toSet()); // 自动去重并收集为Set(底层是HashSet)
// 输出:
// glassBottle: [埃塞俄比亚, 哥伦比亚, 埃塞俄比亚](保留重复和顺序)
// sealedBag: [哥伦比亚, 埃塞俄比亚](去重且无序)
解读:toCollection
像 “容器转换器”,按需把数据装入不同 “容器”,满足多样化存储需求。
技巧 9:多条件过滤(filter
组合)—— 筛选特定口味咖啡
场景:顾客想喝一杯 价格≤30 元 且 带果香 的咖啡,需要双重筛选。
// 定义咖啡类(包含名称、价格、是否果香)
class Coffee {
private String name; // 名称(如“肯尼亚AA”)
private double price; // 价格
private boolean hasFruity; // 是否有果香
public Coffee(String name, double price, boolean hasFruity) {
this.name = name;
this.price = price;
this.hasFruity = hasFruity;
}
public double getPrice() { return price; }
public boolean isHasFruity() { return hasFruity; } // 注意:布尔类型getter通常以is开头
@Override
public String toString() { return name; }
}
// 模拟咖啡列表
List<Coffee> coffees = Arrays.asList(
new Coffee("埃塞俄比亚耶加雪菲", 32.0, true), // 价格超30,果香✔️
new Coffee("肯尼亚AA", 28.0, true), // 价格≤30,果香✔️
new Coffee("哥伦比亚", 25.0, false) // 价格≤30,果香❌
);
// 双重筛选:先过滤价格,再过滤果香
List<Coffee> matchedCoffees = coffees.stream()
.filter(coffee -> coffee.getPrice() <= 30) // 中间操作:价格达标
.filter(coffee -> coffee.isHasFruity()) // 中间操作:果香达标
.collect(Collectors.toList()); // 终止操作:收集结果
// 输出:[肯尼亚AA]
解读:多重 filter
像 “层层筛子”,先筛价格,再筛风味,精准找到顾客的 “梦中情咖”。
技巧 10:扁平化处理(flatMap
)—— 合并多个顾客的订单
场景:咖啡店收到多个顾客的子订单(每个顾客可能点多杯),需要合并成一个总订单。
// 模拟嵌套订单(外层是顾客列表,内层是每个顾客的点单列表)
List<List<String>> customerOrders = Arrays.asList(
Arrays.asList("拿铁", "美式"), // 顾客1的订单
Arrays.asList("卡布奇诺", "手冲") // 顾客2的订单
);
// 扁平化合并:将每个子订单“摊开”成一个大列表
List<String> allOrders = customerOrders.stream()
.flatMap(orders -> orders.stream()) // 中间操作:将每个子列表转换为Stream并合并
// 等价于方法引用:.flatMap(List::stream)
.collect(Collectors.toList()); // 终止操作:收集总订单
// 输出:[拿铁, 美式, 卡布奇诺, 手冲]
解读:flatMap
像 “订单合并大师”,把多层订单 “拍扁” 成一层,再也不用处理嵌套循环。
三、Stream 与传统代码对比:谁更 “香”?
场景 | Stream 代码 | 传统代码 |
---|---|---|
筛选 + 转换 | 链式操作,一行逻辑清晰表达 | 多层循环 + 条件判断,代码冗长 |
分组 + 聚合 | groupingBy 一行搞定 | 手动维护 Map,易错且繁琐 |
去重 + 排序 | distinct().sorted() 流畅衔接 | 借助 Set 和 Collections.sort (),步骤多 |
结语
Stream 流式编程不仅能让代码更简洁,还能让开发过程充满 “仪式感”—— 就像冲一杯好咖啡,关键在于选对工具和流程。掌握这 10 个技巧,你将告别繁琐的循环嵌套,用更优雅的方式处理数据。下次写集合操作时,不妨对自己说:“今天来点 Stream 特调吧!” ☕️
如果本文对您有所帮助,欢迎点赞收藏加关注!您的每一次点击都是对我最大的鼓励。