Java Stream 流式编程入门:10 个基础技巧让代码简洁高效

引言

        想象一下,你是一位咖啡师,面对一堆咖啡豆(集合数据),传统的处理方式是用勺子一颗颗筛选、研磨(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 特调吧!” ☕️

        如果本文对您有所帮助,欢迎点赞收藏加关注!您的每一次点击都是对我最大的鼓励。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员岳彬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值