Java8 新特性(二)- Stream
Stream 用来处理集合数据的,通过 stream 操作可以实现 SQL 的拥有的大部分查询功能
下面借助例子,演示 stream 操作
Java userList 列表
private List<User> userList = Arrays.asList( new User(101, "小明", 10, "男", "青海省", "西宁市"), new User(102, "小青", 12, "女", "宁夏回族自治区", "银川市"), new User(103, "小海", 8, "男", "西藏自治区", "拉萨市"), new User(108, "阿***", 18, "女", "西藏自治区", "拉萨市"), new User(104, "小阳", 9, "女", "新疆维吾尔自治区", "乌鲁木齐市"), new User(105, "小强", 14, "男", "陕西省", "西安市"), new User(106, "小帅", 15, "男", "河北省", "石家庄市"), new User(107, "小云", 15, "女", "河北省", "石家庄市") );
MySQL user 表数据
DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(11) PRIMARY KEY, `name` varchar(20), `age` int(2), `gender` varchar(10), `province` varchar(100), `city` varchar(100) ) ; INSERT INTO `user` VALUES (101, '小明', 10, '男', '青海省', '西宁市'); INSERT INTO `user` VALUES (102, '小青', 12, '女', '宁夏回族自治区', '银川市'); INSERT INTO `user` VALUES (103, '小海', 8, '男', '西藏自治区', '拉萨市'); INSERT INTO `user` VALUES (104, '小阳', 9, '女', '新疆维吾尔自治区', '乌鲁木齐市'); INSERT INTO `user` VALUES (105, '小强', 14, '男', '陕西省', '西安市'); INSERT INTO `user` VALUES (106, '小帅', 15, '男', '河北省', '石家庄市'); INSERT INTO `user` VALUES (107, '小云', 15, '女', '河北省', '石家庄市');
查询字段 select - map
// select id from user userList.stream() .map(e -> e.getId()) .forEach(System.out::println);
至于如何实现 select id, name from user
查询多字段在下面 collector 收集器会详细讲解
条件 where - filter
// select * from user where age<10 userList.stream() .filter(e-> e.getAge() < 10) .forEach(System.out::println); // select * from user where age<10 and gender='男' userList.stream() .filter(e->e.getAge() < 10) .filter(e->e.getGender()=="男") .forEach(System.out::println);
最值、总和、数量、均值(max, min, sum, count, average)
// select max(age), min(age), sum(age), count(age), avg(age) from user // max Optional<Integer> maxAge = userList.stream() .map(e -> e.getAge()) .max(Comparator.comparingInt(x -> x)); // 等同于 // Optional<Integer> maxAge = userList.stream() // .map(e -> e.getAge()) // .max((x, y) -> x-y); // min Optional<Integer> minAge = userList.stream() .map(e -> e.getAge()) .min(Comparator.comparingInt(x -> x)); // sum Optional<Integer> sumAge = userList.stream() .map(e -> e.getAge()) .reduce((e, u) -> e + u); // count long count = userList.stream() .map(e -> e.getAge()) .count(); // 平均值=总和/数量
排序 order by - sorted
// select * from user order by age userList.stream() .sorted(Comparator.comparingInt(User::getAge)) .forEach(System.out::println);
分页 limit - skip、limit
// select * from user limit 5 userList.stream() .limit(5) .forEach(System.out::println); // select * from user limit 5, 5 userList.stream() .skip(5) .limit(5) .forEach(System.out::println); // select * from user order by age limit 1 userList.stream() .sorted(Comparator.comparingInt(User::getAge)) .limit(1) .forEach(System.out::println); // 或者 Optional<User> minAgeUser = userList.stream() .sorted(Comparator.comparingInt(User::getAge)) .findFirst();
是否存在 exists - anymatch
// select exists(select * from user where name='小海') // 有没有名字叫“小海”的用户 boolean exists0 = userList.stream() .anyMatch(e -> e.getName().equals("小海")); // select not exists(select * from user where name='小海') // 是不是没有名字叫“小海”的用户 boolean exists1 = userList.stream() .noneMatch(e -> e.getName().equals("小海")); // 是不是所有用户年龄都小于10岁 boolean exists2 = userList.stream() .allMatch(e -> e.getAge() < 10);
收集操作 collect
收集操作就是遍历 stream 中的元素,并进行累加处理,即归约 reduction
A reduction operation (also called a fold) takes a sequence of input elements and combines them into a single summary result by repeated application of a combining operation, such as finding the sum or maximum of a set of numbers, or accumulating elements into a list.
前面提到的 max()
min()
count()
reduce()
都属于 reduction operation
但 collect()
又和前面这几种归约操作有所区别,它是 Mutable reduction 动态归约
A mutable reduction operation accumulates input elements into a mutable result container, such as a
Collection
orStringBuilder
, as it processes the elements in the stream
区别:动态归约将结果放进 Collection
StringBuilder
这样的动态容器中,所以称为动态归约。
Stream 接口提供了两个 collect() 方法
<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner); <R, A> R collect(Collector<? super T, A, R> collector);
我们只需理解了第一个方法,第二个方法就手到擒来了
理解第一个 collect 方法,强烈建议阅读文档 动态归约的定义,下面只简单的介绍一下它
三个参数:
- 供给者 supplier:负责提供动态容器,例如 Collectors、StringBuilder
- 累加器 accumulator:负责将流中的元素做累加处理
- 合并者 combiner :负责将两个容器的元素合并在一起
在串行流中,combiner 根本没有执行,所以随便写点啥满足参数对象就行。
如果说串行流是单线程,那么并行流就是多线程了
举个例子:
ArrayList<String> strings = new ArrayList<>(); for (T element : stream) { strings.add(element.toString()); } // 等同于 ArrayList<String> strings = stream.collect(() -> new ArrayList<>(), (c, e) -> c.add(e.toString()), (c1, c2) -> c1.addAll(c2));
与其传递三个参数这么麻烦,还不如直接传递一个对象呢!
这就是第二个 collect()
方法的由来,使用收集器 Collector 来替代三个参数
实际上,我们一般不需要自己创建 Collector 对象,Java8 提供了一个 Collectors 类,专门提供收集器 Collector 对象。毕竟我们平时能够使用到的收集操作也就那几种:转为集合对象、分组、统计。
下面以例子演示
在初看 stream 操作的时候,我被什么创建、中间操作、终止操作、不会改变原对象给弄晕了,我根本不关心这些,我的第一想法是怎么将操作后的数据导出来,重新变成集合对象。
toCollection
不使用收集器的情况下:
List<User> subUserList1 = userList.stream() .filter(e -> e.getAge() < 10) .filter(e -> e.getGender() == "男") .collect(() -> new ArrayList<>(), (c, e) -> c.add(e), (c1, c2) -> c1.addAll(c2));
在 collect() 方法第二个参数累加器 accumulator (c, e) -> c.add(e)
这里,对流中元素进行了遍历,所以可以把流中元素添加到任意的集合容器中,List、Set、Map 等等
使用 Collectors 工具类提供的收集器:
// toList() List<User> list = userList.stream() .filter(e -> e.getAge() < 10) .filter(e -> e.getGender() == "男") .collect(Collectors.toList()); // toSet() Set<User> set = userList.stream() .filter(e -> e.getAge() < 10) .filter(e -> e.getGender() == "男") .collect(Collectors.toSet()); // toCollection(),想要返回什么容器,就 new 一个 ArrayList<User> collection = userList.stream() .filter(e -> e.getAge() < 10) .filter(e -> e.getGender() == "男") .collect(Collectors.toCollection( () -> new ArrayList<>() ));
这里插播一条新闻:如何将流转为数组?
Stream
提供了方法 toArray()
Object[] toArray(); <A> A[] toArray(IntFunction<A[]> generator);
小试牛刀:
Object[] nameArray = userList.stream() .map(e -> e.getName()) .toArray(); Arrays.stream(nameArray) .forEach(System.out::println); // 转为 User 对象数组 User[] users = userList.stream() .filter(e -> e.getGender() == "女") .toArray(User[]::new); Arrays.stream(users) .forEach(System.out::println);
toStringBuilder
不使用收集器的情况下:
StringBuilder joinName = userList.stream() .map(e -> e.getName()) .collect(StringBuilder::new, (s, e) -> s = s.length() > 0 ? s.append("-" + e) : s.append(e), (s1, s2) -> s1.append(s2) );
谁能告诉我在Java中怎么单独使用三元运算符?s = s.length() > 0 ? s.append("-" + e) : s.append(e)
我想把 s =
省略掉,但 Java 中不行
使用 Collectors
类提供的收集器:
String joinName1 = userList.stream() .map(e -> e.getName()) .collect(Collectors.joining()); String joinName2 = userList.stream() .map(e -> e.getName()) .collect(Collectors.joining("-")); String joinName3 = userList.stream() .map(e -> e.getName()) .collect(Collectors.joining("-", "[", "]"));
至于 Collectors.joining() 参数分别代表什么含义,看一下它们的参数名称,就明白了
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter, // 分隔符 CharSequence prefix, // 前缀 CharSequence suffix) // 后缀
toMap
在 Collectors 中一共有3个 toMap()
,它们用来处理不同的问题
两个参数的 toMap
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) { return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new); }
参数
keyMapper
用来获取key;valueMapper
用来获取 value它的内部调用了四个参数的 toMap() 方法
例子
Map<Integer, User> map1 = userList.stream() .collect(Collectors.toMap(e -> e.getId(), Function.identity())); System.out.println(map1); // Function.identity() 等价于 e -> e // select id, name, gender from user Map<Integer, Map<String, Object>> map2 = userList.stream() .collect(Collectors.toMap(e -> e.getId(), e -> { Map<String, Object> map = new HashMap<>(); map.put("gender", e.getGender()); map.put("name", e.getName()); map.put("id", e.getId()); return map; })); System.out.println(map2);
你:如果 key 冲突了咋办?
Java8:你想咋办就咋办
三个参数的 toMap
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction) { return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new); }
第三个参数
mergeFunction
就是用来处理 key 键冲突的内部也是调用了四个参数的 toMap() 方法
例子
// 如果 key 冲突,那么将冲突的 value 值拼接在一起 Map<String, String> map3 = userList.parallelStream() .collect(Collectors.toMap( e -> e.getGender(), e -> e.getName(), (o1, o2) -> o1 + ", " + o2 ) ); System.out.println(map3);
你:我想自己 new 一个 Map 对象
四个参数的 toMap
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier)
参数
mapSupplier
用来提供返回容器
例子
LinkedHashMap<String, String> map4 = userList.parallelStream() .collect(Collectors.toMap(e -> e.getGender(), e -> e.getName(), (o1, o2) -> o1 + ", " + o2, LinkedHashMap::new)); System.out.println(map4);
reducing
单参数和两参数的 reducing()
Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op) Collector<T, ?, U> reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator<U> op)
以例子具体解释这两个方法
Optional<String> names1 = userList.stream() .map(User::getName) .collect(Collectors.reducing((e1, e2) -> e1 + "," + e2)); System.out.println(names1.get()); // 等同于 String names2 = userList.stream() .collect(Collectors.reducing( "", (e) -> e.getName(), (e1, e2) -> e1 + "," + e2) ); System.out.println(names2);
输出结果:
小明,小青,小海,阿***,小阳,小强,小帅,小云 ,小明,小青,小海,阿***,小阳,小强,小帅,小云
参数
identity
表示返回结果的初始值
三参数的 reducing()
reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator<U> op)
identity
是初始值,mapper
会对元素先进行一次处理,然后 op
对元素进行归约操作
注意: 返回类型要和参数
identity
的一致。
你也许会纳闷,为什么有的返回一个Optional<String>
类型数据,而有的就返回了String
因为含有参数identity
的 reduing 方法中返回值有初始值,也就是 identity,所以不会出现空的情况
下面Collectors 提供的一些常用归约收集器
// minBy、maxBy Optional<User> minAgeUser = userList.stream() .collect(Collectors.minBy((o1, o2) -> o1.getAge() - o2.getAge())); // counting Long count = userList.stream() .collect(Collectors.counting()); // summingInt、summingLong、summingDouble、 Integer sumAge = userList.stream() .collect(Collectors.summingInt(User::getAge)); // averagingInt、averagingLong、averagingDouble // 平均值内部是总值/数量,所以返回值是浮点数 dobule Double avgAge = userList.stream() .collect(Collectors.averagingInt(User::getAge));
你也许觉得每次都要执行一遍 minBy、maxBy、counting、summingXxx、averagingXxx 这些太麻烦了,有没有一次执行就获取所有这些方法结果?
有的。这就是 summarizingXxx
Collector<T, ?, IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper) Collector<T, ?, LongSummaryStatistics> summarizingLong(ToLongFunction<? super T> mapper) Collector<T, ?, DoubleSummaryStatistics> summarizingDouble(ToDoubleFunction<? super T> mapper)
这里不演示了,实际上你看一下 XxxSummaryStatistics
这些类就明白了,比如
public class IntSummaryStatistics implements IntConsumer { private long count; private long sum; private int min = Integer.MAX_VALUE; private int max = Integer.MIN_VALUE; ... }
group by
最最激动人心的时候到了,我们要使用分组了!!!
Map<String, List<User>> map = userList.stream() .collect(Collectors.groupingBy(User::getGender));
SQL 中的 group by
结果集中只能包含分组字段和聚合函数计算结果,这段代码比它更加全面
我们使用如下语句输出结果
map.keySet().stream() .forEach((e) -> { System.out.println(e + "=" + map.get(e)); });
显示结果:
女=[User{id=102, name='小青', age=12, gender='女', province='宁夏回族自治区', city='银川市'}, User{id=108, name='阿***', age=18, gender='女', province='西藏自治区', city='拉萨市'}, User{id=104, name='小阳', age=9, gender='女', province='新疆维吾尔自治区', city='乌鲁木齐市'}, User{id=107, name='小云', age=15, gender='女', province='河北省', city='石家庄市'}] 男=[User{id=101, name='小明', age=10, gender='男', province='青海省', city='西宁市'}, User{id=103, name='小海', age=8, gender='男', province='西藏自治区', city='拉萨市'}, User{id=105, name='小强', age=14, gender='男', province='陕西省', city='西安市'}, User{id=106, name='小帅', age=15, gender='男', province='河北省', city='石家庄市'}]
它真的分组了!!这是真正的分组
那怎么对分组中的元素进行操作呢,像 SQL 那样??
完全不用担心,Collectors 提供了三个 groupBy 方法返回分组收集器
Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T,? extends K> classifier) Collector<T, ?, Map<K, D>> groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream) Collector<T, ?, M> groupingBy(Function<? super T,? extends K> classifier, Supplier<M> mapFactory, Collector<? super T,A,D> downstream)
让我们放飞想象的翅膀,思考一下这几个参数分别有什么用。
downstream ?有 down 就表示有 up。那么谁是 upstream,很明显是 userList.stream
,那么 downstream 就是分组集合的流喽。猜测 downstream 收集器是对分组中的元素进行归约操作的,就像是分组 SQL 语句字段中的聚合操作一样。
// select gender, count(*) from user group by gender Map<String, Long> map2 = userList.stream() .collect(Collectors.groupingBy(User::getGender, Collectors.counting())); System.out.println(map2);
输出结果确实不出所料!这就是证明参数 downstream
确实是分组集合元素的收集器。
Supplier<M> mapFactory
这函数式接口方法不会有参数传入,所以不会操作集合元素;它只是返回一个变量。同志们,注意观察三个方法返回值,前二者都指定了 Map 作为归约操作的返回类型,而第三个要我们自己定义,使用 mapFactory
提供返回的数据容器
两参数的 groupingBy
方法其实是调用了三参数的 groupingBy
方法(而单参数 groupingBy
调用了两参数的 groupingBy
)
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream) { return groupingBy(classifier, HashMap::new, downstream); }
groupingBy
经常使用 Collectors.mapping()
处理分组集合
Map<String, List<Map<String, Object>>> map4 = userList.stream() .collect(Collectors.groupingBy( User::getGender, Collectors.mapping(e -> { Map<String, Object> m = new HashMap<>(); m.put("name", e.getName()); m.put("id", e.getId()); return m; }, Collectors.toList()) )); System.out.println(map4);
输出结果:
{女=[{name=小青, id=102}, {name=阿***, id=108}, {name=小阳, id=104}, {name=小云, id=107}], 男=[{name=小明, id=101}, {name=小海, id=103}, {name=小强, id=105}, {name=小帅, id=106}]}
partitionBy
实际上也是分组,只不过 partitionBy 是按照布尔值(真假)来分组
Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) { return partitioningBy(predicate, toList()); } Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate, Collector<? super T, A, D> downstream)
例子:大于10岁为一组,小于等于10的为一组
Map<Boolean, List<User>> map1 = userList.stream() .collect(Collectors.partitioningBy(e -> e.getAge() > 10));
例子:统计大于10岁的有多少人,小于等于10岁的有多少人
Map<Boolean, Long> map2 = userList.stream() .collect(Collectors.partitioningBy(e -> e.getAge() > 10, Collectors.counting()));
第二个参数 downstream
用来处理分组集合
结语
Java8 提供的 stream 几乎是穷尽了所有集合元素能有的操作,起码是穷尽了我脑海里对集合元素操作的所有想象
这篇文章也列举了 stream 绝大部分的功能,尽量写得通俗易懂,但读者理解起来可能还是有模糊的地方,这时建议大家参考 Java8 API 官方文档 ,多做几个 Demo 加深理解
不要过度使用
stream 是为了方便集合操作,简化代码而推出的,提升代码执行效率并不是它的目的。
虽然,并行流会对代码的执行效率有较大的提升(尤其是数据量非常大的时候),但也依赖于计算机的CPU配置。
Stream 能实现的功能,for 循环都能实现,只是 Stream 代码一般比较简洁,可读性强。但在某些情况下,使用 for 循环要比 Stream 要简洁代码逻辑清晰
举个例子:
private List<Order> orderList = Arrays.asList( new Order(103), new Order(106), new Order(107), new Order(104), new Order(102), new Order(103), new Order(102), new Order(101), new Order(104), new Order(102), new Order(105) ); // 现根据 userId 设置 Order 对象的 name 属性 // 使用 stream List<Order> newOrderList = orderList.stream() .map(o -> userList.stream() .filter(u -> u.getId() == o.getUserId()) .findFirst() .map(u -> { o.setUserName(u.getName()); return o; }) .orElse(o)) .collect(Collectors.toList()); newOrderList.stream().forEach(System.out::println); // 使用 for 循环 for (Order o : orderList) { for (User u : userList) { if (o.getUserId() == u.getId()) { o.setUserName(u.getName()); break; } } } orderList.stream().forEach(System.out::println);
在这个例子中,使用 for 循环要比 使用 stream 干净利落的多,代码逻辑清晰简明,可读性也比 stream 好。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律