引言
本文旨在介绍 Java Stream API 的基本概念及其强大之处,同时探讨如何利用这一特性来编写更加简洁、高效的代码。我们将从 Stream API 的基本原理讲起,逐步深入到实际应用中的高级技巧,包括如何创建 Stream、中间操作与终结操作的区别,以及并行流的使用等。本文包含了个人实际开发中所常用的api,无论你是 Java 初学者还是有经验的开发者,都将在本文中找到有价值的内容,帮助你在日常开发中更好地利用 Stream API。本人尽量采用上下文低耦合的例子,部分参数已按照命名规范定义,方便各位读者阅读。
废话不多说,咱们进入主题吧!
一. 流创建
1. Stream创建
Stream创建
List<String> list = new ArrayList<>();
Stream<String> stream = list.stream(); //串行流
Stream<String> parallelStream = list.parallelStream(); //并行流
Stream<Integer> stream1 = Stream.of(1,2,3,4,5);
2、Collection集合创建(应用中最常用的一种)
List<Integer> integerList = new ArrayList<>();
integerList.add(1);
integerList.add(2);
integerList.add(3);
integerList.add(4);
integerList.add(5);
Stream<Integer> listStream = integerList.stream();
3、Array数组创建
int[] intArr = {1, 2, 3, 4, 5};
IntStream arrayStream = Arrays.stream(intArr);
二. 操作符
流的操作类型主要分为两种:中间操作符、终端操作符
中间操作符
通常对于Stream的中间操作,可以视为是源的查询,并且是懒惰式的设计,对于源数据进行的计算只有在需要时才会被执行,与数据库中视图的原理相似;
Stream流的强大之处便是在于提供了丰富的中间操作,相比集合或数组这类容器,极大的简化源数据的计算复杂度
一个流可以跟随零个或多个中间操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用
这类操作都是惰性化的,仅仅调用到这类方法,并没有真正开始流的遍历,真正的遍历需等到终端操作时,常见的中间操作有下面即将介绍的 filter、map 等
筛选(filter)
filter
是 Java Stream API 中的一个方法,用于对流中的元素进行过滤。它接受一个谓词(Predicate),即一个返回布尔值的函数,用于判断元素是否应该保留在流中
举例:
-
// 过滤出大于7的数字并打印 List<Integer> list = Arrays.asList(6, 7, 3, 8, 1, 2, 9); list.stream.filter(x -> x > 7).forEach(System.out::println);
-
// 过滤出元素非空的集合 List<String> nonBlankPaths = chunkPaths.stream().filter(StringUtils::isNotBlank).collect(Collectors.toList());
解释:
.filter(StringUtils::isNotBlank)
将isNotBlank
方法作为谓词传递给filter
方法。这意味着流中的每个元素都会被传递给isNotBlank
方法,如果isNotBlank
返回true
,则该元素会被保留在流中;否则,该元素会被过滤掉
映射(map/flatMap)
举例:
-
// 将所有用户对象根据id映射组成set集合(去重) Set<Integer> userIds = userMapper.selectList(null).stream().map(User::getId).collect(Collectors.toSet());
-
// 英文字符串数组的元素全部改为大写 String[] strArr = { "abcd", "bcdd", "defde", "fTr" }; List<String> strList = Arrays.stream(strArr).map(String::toUpperCase).collect(Collectors.toList());
去重(distinct)
返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流
举例:
List<String> distinctUsers = userList.stream()
.map(User::getCity)
.distinct()
.collect(Collectors.toList());
distinctUsers.forEach(System.out::println);
排序(sorted)
sorted()
方法可以用来对流中的元素进行排序。sorted()
方法接受一个Comparator
对象作为参数,该对象定义了元素之间的比较逻辑。如果没有提供Comparator
对象,则默认使用自然顺序进行排序(如果元素实现了Comparable
接口)
举例:
-
List<String> strings = new ArrayList<String>() {{ add("222");add("666");add("444");add("111");add("333");add("555"); }}; // sorted()默认升序排序 List<String> sortedStrings = strings.stream().sorted().collect(Collectors.toList());
-
// 根据用户年龄升序排序 List<User> sortedUsers = users.stream() .sorted(Comparator.comparing(e -> e.getAge())) .collect(Collectors.toList()); // 等效写法如下 List<User> sortedUsers = users.stream() .sorted(Comparator.comparing(User::getAge)) .collect(Collectors.toList()); // 根据用户年龄降序排序(先升序,再逆序) List<User> collect = users.stream() .sorted(Comparator.comparing(User::getAge).reversed()) .collect(Collectors.toList()); // 根据用户年龄降序排序(直接降序) List<User> collect2 = users.stream() .sorted((x, y) -> y.getAge().compareTo(x.getAge())) .collect(Collectors.toList());
限流(limit)
会返回一个不超过给定长度的流,说白了就是自定义返回个数
userList.stream().limit(5).collect(Collectors.toList()).forEach(System.out::println);
peek
它的主要功能是对流中的每个元素执行一个操作(可以是获取、修改或打印等),而不影响流的整体处理流程。
// 对集合非空的元素进行打印信息
addrList.stream()
.filter(Objects::nonNull)
.peek(info -> {
System.out.println("Processing Element: " + info);
}).collect(Collectors.toList());
进阶:
// 查询聊天记录后将集合中的所有readFlag已读标识变为true
chatMapper.selectFriendChats(userFriendIds)
.stream().peek(chat -> chat.setReadFlag(true)).collect(Collectors.toList())
注意:peek()方法是 Strean 接口中的一个中间操作,它允许你在流的每 个元素上执行一个操作,但是这个操作是在最终的终端操作(如forEach,collect,limit 等)执行 前进行的。 然而,如果 peek()是流中唯一的操作,那么它实际上不会执行。这是因为
peek ()本身并不是一个 终端操作,它不会触发流的执行。在 jav 8 及以后的版本中,流的执行是情性的,这意味着流操作
不会立即执行,而是在遇到终端操作时才会实际执行。
终端操作符
tream流执行完终端操作之后,无法再执行其他动作,否则会报状态异常,提示该流已经被执行操作或者被关闭,想要再次执行操作必须重新创建Stream流
一个流有且只能有一个终端操作,当这个操作执行后,流就被关闭了,无法再被操作,因此一个流只能被遍历一次,若想在遍历需要通过源数据在生成流。
终端操作的执行,才会真正开始流的遍历。如 count、collect 等
遍历/匹配(foreach/find/match)
// 遍历输出符合条件的元素
list.stream().filter(x -> x > 6).forEach(System.out::println);
收集(collect)
收集器,将流转换为其他形式
Set set = userList.stream().collect(Collectors.toSet());
List list = userList.stream().collect(Collectors.toList());
list.forEach(System.out::println);
聚合(max/min/count)
举例:
-
// 过滤出大于6的数字数量 List<Integer> list = Arrays.asList(7, 6, 4, 8, 2, 11, 9); long count = list.stream().filter(x -> x > 6).count();
-
// 获取String集合中最长的元素 List<String> list = Arrays.asList("adnm", "admmt", "pot", "xbangd", "weoujgsd"); Optional<String> max = list.stream().max(Comparator.comparing(String::length));
归约(reduce)
可以将流中元素反复结合起来,得到一个值
List strings = Arrays.asList(“cv”, “abd”, “aba”, “efg”, “abcd”,“jkl”, “jkl”);
Optional reduce = strings.stream().reduce((acc,item) -> {
return acc+item;
});
if(reduce.isPresent())
out.println(reduce.get());
Collectors
Collector:结果收集策略的核心接口,具备将指定元素累加存放到结果容器中的能力;并在Collectors工具中提供了Collector接口的实现类
归集(toList/toSet/toMap)
举例:
-
// key: 用户ID value: 礼物名称 Map<Integer, String> productNameMap = orders.stream().collect(Collectors.toMap(Order::getUserId, order -> order.getProduct().getProductName()));
-
// Set集合去重 Set<String> userIdSet = users.stream().map(User::getUserId).collect(Collectors.toSet());
Collectors.joining
以指定分隔符链接成字符串
举例:
List<String> list1 = new ArrayList<>();
list1.add("1111");
list1.add("222");
list1.add("1113331");
list1.add("444");
list1.add("555");
List<String> list2 = new ArrayList<>();
list2.add("1111");
list2.add("222");
list2.add("1113331");
list2.add("444");
list2.add("666");
String collect1 = list1.stream().collect(Collectors.joining());
String collect2 = list2.stream().collect(Collectors.joining(","));
System.out.println(collect1);
System.out.println(collect2);
//控制台打印结果
11112221113331444555
1111,222,1113331,444,666
Collectors.groupingBy
按条件分组,根据字段对集合进行分组;
Map<String,List<User>> groupCity = userList.stream().collect(Collectors.groupingBy(User::getCity));
番外
整理了一些大家常见的疑问,为大家倾情献上!希望能对各位读者有所帮助!
1. list.stream().forEach()和list.forEach()的区别
list.stream().forEach() 和 list.forEach() 在 Java 中都是用于遍历集合元素的方法,但它们在使用场景和功能上有所不同:
list.forEach():
是从 Java 8 开始引入到 java.util.List 接口的标准方法。
直接对列表进行迭代,它采用内部迭代的方式,不需要显式创建迭代器。
使用 Lambda 表达式或方法引用来处理集合中的每个元素。
这个操作是同步的,并且在执行过程中不会创建 Stream 流对象,因此没有额外的流管道开销。
list.stream().forEach():
首先将列表转换为一个 Stream 对象,这是 Java 8 引入的 Stream API 的一部分。
转换为 Stream 后可以利用 Stream API 提供的一系列丰富操作,如 filter、map、sorted 等,然后再调用 forEach 方法。
尽管最终也是对集合元素进行迭代,但它允许你在一个惰性计算(lazy computation)的上下文中工作,也就是说,Stream 操作可以在需要时才进行计算,这有助于优化性能,特别是在链式操作中。
同样接受 Lambda 表达式来处理每个元素,但因为涉及到了 Stream,所以提供了更多的函数式编程能力,比如并行处理(parallel processing),通过调用 .parallel().forEach() 可以在多核环境下并行执行操作。
简单来说就是:
list.forEach()节省了创建流的开销,如果简单对集合进行遍历则可以使用。而list.stream().forEach()当你想要利用 Stream API 的特性(如函数式编程风格、延迟计算、并行处理等)处理复杂集合逻辑,则应该使用list.stream().forEach()。
2. Java17的stream流里toList和.collect(Collectors.toList())有什么不同
.collect(Collectors.toList());
源码:
public static <T>
Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>(ArrayList::new, List::add,
(left, right) -> { left.addAll(right); return left; },
CH_ID);
}
解释:显而易见,Collectors.toList()返回的是new一个ArrayList集合
.tolist()
default List<T> toList() {
return (List<T>) Collections.unmodifiableList(
new ArrayList<>(Arrays.asList(this.toArray())));
}
@SuppressWarnings("unchecked")
public static <T> List<T> unmodifiableList(List<? extends T> list) {
if (list.getClass() == UnmodifiableList.class || list.getClass() == UnmodifiableRandomAccessList.class) {
return (List<T>) list;
}
return (list instanceof RandomAccess ?
new UnmodifiableRandomAccessList<>(list) :
new UnmodifiableList<>(list));
}
解释:但是tolist收集出来的list实际类是UnmodifiableList这个类
根据某神秘男子的解释:
UnmodifiableList
是 Java 中的一个类,用于创建不可修改的列表。它实际上是通过Collections.unmodifiableList()
方法创建的。一旦列表被创建为不可修改的,就无法对其进行添加、删除或修改元素的操作
总结:
1.如果你想在之后的代码中对集合进行更改操作,则需要使用**.collect(Collectors.toList())**
2.如果你的集合已经不想被更改,则使用toList()集合
个人建议
想要熟练使用StreamAPI,不妨学习csdn收藏量靠前blog的写法,在开发的过程中处理集合尽量使用相关语句。等到熟悉基础语法后,结合Lambda表达式可以飞速提升开发效率,并且让别人觉得你是个掌握Java语言特性的高手。包括本人在开发中遇到繁杂的业务逻辑,也会参考各大开发社区的学习文档,所以技术没有上限,尽量提升自己吧!
参考文档
java stream使用指南-------sorted使用及进阶
这么简单,还不会使用java8 stream流的map()方法吗?
Java–Stream流详解
Java8中Collectors详解
list.stream().forEach()和list.forEach()的区别
Java17的stream流里toList和.collect(Collectors.toList())有什么不同