Java 8 Stream API:从基础到高级,掌握流处理的艺术

一、Stream(流)基本介绍

Java 8 API 添加了一个新的抽象称为Stream(流),可以让你以一种声明的方式处理数据,这种风格将要处理的元素集合看做一种流,元素流在管道中传输,并在管道中间的节点上经过中间操作(intermediate operation)的处理(如:筛选,排序,聚合等),最后由最终操作(terminal operation)得到前面处理的结果。

Stream(流)使用一种类似于SQL语句从数据库查询数据的直观方式来提供一种对Java集合运算和表达的高阶抽象。

  • 元素是特定类型的对象,形成一个队列;
  • Java中的Stream并不会存储元素,而是按需计算;
  • 数据源可以是集合,数组,I/O channel,产生器generator等;
  • 很多中间操作的方法返回类型就是Stream,因此可以直接连接起来,如下图:

在这里插入图片描述

  • 流的操作不会改变原集合,会生产新的集合,List<String> newList = list.stream().xxx

Stream API 可以极大提高Java程序员的生产力,让程序员写出高效率、干净、整洁的代码。

二、Stream(流)的常用方法

// 准备测试类和数据
public class User{
    String name;
    Integer age;
}
List<User> userList = new ArrayList<>();
userList.add(new User("孙悟空", 500));
userList.add(new User("沙悟净", 600));
userList.add(new User("猪八戒", 400));
List<String> letterList = Arrays.asList("a", "", "b", "c", "d", "", "e");
List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5);

1、filter(element -> boolean表达式)

  • 过滤元素,将符合boolean表达式的元素保存下来。
// 过滤字母集合,过滤掉空字符串,结果为:["a","b","c","d","e"]
List<String> newLetterList = letterList.stream().filter(str -> !str.isEmpty()).collect(Collectors.toList());

2、distinct

  • 去重,这个方法是元素自身的equals方法来判断其元素是否相等。
userList = userList.stream().distinct().collect(Collectors.toList);

如果这里不重写User类的equals方法,相同的数据不会被处理。

3、sorted() / sorted((T, T) -> int)

  • 对流中的元素进行排序,若流中元素的类有自己的排序规则(即实现了Comparable接口)可直接sorted(),否则需要用sorted((T,T) -> int)说明排序规则。
// 根据年龄大小来排序
userList = userList.stream().sorted((u1,u2) -> u1.getAge() - u2.getAge()).collect(Collectors.toList());

// 也可直接替换为方法引用
userList = userList.stream().sorted(Comparator.comparingInt(User::getAge)).collect(Collectors.toList());

4、min、max

  • min() 和 max() 方法,用于查找流中的最小值和最大值。这些方法返回一个 Optional 对象,包含流中的最小或最大元素。

min(Comparator<? super T> comparator) 方法接受一个比较器作为参数,用于定义元素之间的顺序。它会遍历整个流,并返回其中的最小元素。如果流为空,则返回一个空的 Optional 对象。

max(Comparator<? super T> comparator) 方法与 min() 方法类似,唯一的区别是它返回流中的最大元素。同样地,如果流为空,则返回一个空的 Optional 对象。

以下是一个示例,展示如何使用 min() 方法找到一个字符串流中的最短字符串:

List<String> strings = Arrays.asList("apple", "banana", "cherry", "date");
Optional<String> shortestString = strings.stream().min((s1, s2) -> s1.length() - s2.length());
if (shortestString.isPresent()) {
    System.out.println("Shortest string: " + shortestString.get());
} else {
    System.out.println("No strings in the list.");
}

在这个例子中,我们首先创建了一个包含四个字符串的列表。然后,使用 stream() 方法将其转换为一个流。接着,调用 min() 方法并传入一个比较器,用于比较两个字符串的长度。最后,使用 isPresent() 方法检查是否找到了最短字符串,并打印结果。

5、summaryStatistics

  • summaryStatistics()它可以对数值型数据流进行统计汇总。这个方法返回一个 IntSummaryStatistics、LongSummaryStatistics 或 DoubleSummaryStatistics 对象,具体取决于流中元素的类型。

以下是一些常见的统计信息:

  • getCount(): 返回流中元素的数量。
  • getSum(): 返回流中所有元素的总和。
  • getAverage(): 返回流中所有元素的平均值。
  • getMin(): 返回流中最小的元素。
  • getMax(): 返回流中最大的元素。

这些方法可以帮助你快速获取流中数值型数据的基本统计信息。

例如,如果你有一个 Stream 对象,想要计算其中所有元素的平均值,你可以这样做:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
double average = numbers.stream()
                       .mapToDouble(i -> i)
                       .summaryStatistics()
                       .getAverage();
System.out.println("Average: " + average);

在这个例子中,我们首先将一个 List 转换为一个 Stream。然后,我们使用 mapToDouble() 方法将每个元素转换为一个 double 类型的值。接着,调用 summaryStatistics() 方法来计算流中所有元素的统计信息。最后,使用 getAverage() 方法获取平均值并打印出来。

同样地,如果你有一个 Stream 对象,想要计算其中所有元素的总和和最小值,你可以这样做:

List<Double> numbers = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0);
DoubleSummaryStatistics stats = numbers.stream()
                                      .summaryStatistics();
double sum = stats.getSum();
double min = stats.getMin();
System.out.println("Sum: " + sum);
System.out.println("Min: " + min);

在这个例子中,我们首先将一个 List 转换为一个 Stream。然后,直接调用 summaryStatistics() 方法来计算流中所有元素的统计信息。最后,使用 getSum() 和 getMin() 方法获取总和和最小值并打印出来。

6、limit(long n)

  • 保留处理结果中的前n个元素。
userList = userList.stream().limit(1).collect(Collectors.toList());

7、skip(long n)

  • 去除(跳过)处理结果中的前n个元素。
// 从处理结果中保留2个元素,再从保留的2个元素中去除第1个元素
userList = userList.stream().limit(2).skip(1).collect(Collectors.toList());

// 从处理结果中去除2个元素后,再保留1个元素
userList = userList.stream().skip(2).limit(1).collect(Collectors.toList());

8、map(T -> R)

  • 将流中的每一个元素映射为R。

map方法接收一个lambda表达式,这个表达式是一个函数,输入类型是集合元素的类型,输出类型是任意类型,即你可以选择将元素映射为任意类型,并对映射后的值做下一步处理。

// 将集合中的每个元素+2,输出结果:[3,4,5,6,7]
numberList = numberList.stream().map(i -> i+2).collect(Collectors.toList());
// 将用户的name、age分别保存到新集合中
List<String> nameList = list.stream().map(Person::getName).collect(Collectors.toList());
List<Integer> ageList = list.stream().map(Person::getAge).collect(Collectors.toList());
// 将用户的属性取出做进一步处理
userList = userList.stream().map(u -> {
	u.setAge(u.getAge() + 1);
	u.setName("孙悟空".equals(p.getName()) ? "悟空" : "西游人物");
	return p;
}).collect(Collectors.toList());

9、flatMap(T -> Stream)

  • flatMap的用法和map类似,它们都接受一个函数作为参数,用于对流中的每个元素进行转换。

map和flatMap的区别:

  • map()操作将每个元素转换成一个新元素,并将所有这些新生成的元素收集到一个新的流中。
  • flatMap()操作将每个元素转换成一个新的流,并将所有这些新生成的流合并成一个单一的流。

flatMap()和map()之间还有一个重要的区别,那就是flatMap()支持处理包含嵌套数据结构的流。

在Java中,如果你有一个泛型类型中的数据本身也是一个泛型类型,例如List<List>,那么使用map()操作时,你可能会遇到一些困难。因为map()操作只会对最外层的元素进行转换,而不会深入到嵌套的数据结构中。

但是,flatMap()操作可以很好地处理这种情况。它可以将每个元素转换成一个流,并将所有这些流合并成一个单一的流。这样,你就可以在处理嵌套数据结构时使用flatMap()操作。

示例:首先使用stream()方法将userPlus转换为一个流,由于userPlus是一个嵌套的集合,所以我们需要使用flatMap()操作来将其展平成一个单一的流,接下来,我们使用map()操作来从每个User对象中提取出其name属性,并将结果转换成一个新的流,再使用distinct()操作来去除重复的名字,最后,我们使用collect()操作将流收集到一个新的List中,这个列表包含了所有唯一的用户名。

List<User> user1 = new ArrayList<>();
user1.add(new User("A",23));
user1.add(new User("B",23));

List<User> user2 = new ArrayList<>();
user2.add(new User("C",23));
user2.add(new User("D",23));

List<List<User>> userPlus = new ArrayList<>();
userPlus.add(user1);
userPlus.add(user2);

// 最后输出的结果是:["A","B","C","D"]
List<String> nameList = userPlus.stream().flatMap(t -> t.stream()).map(t -> t.getName()).distinct().collect(Collectors.toList());

10、reduce((T, T) -> T) / reduce(T, (T, T) -> T)

  • reduce操作可以用来将流中的元素组合成一个单一的结果,可以用来执行各种聚合操作。

这个操作有两种重载形式:

  • reduce((T, T) -> T):这种形式的reduce接受一个二元操作符(即一个函数),该函数将两个元素合并成一个新元素。这个过程会一直重复,直到流中的所有元素都被合并成一个单一的结果。
  • reduce(T, (T, T) -> T): 这种形式的reduce()除了接受一个二元操作符外,还接受一个初始值。如果流为空,初始值将直接作为结果返回;否则,初始值将与流中的第一个元素合并,产生一个新的中间结果,然后再与流中的下一个元素合并,以此类推,直到流中的所有元素都被处理完毕。
// 计算年龄总和
int sum = personList.stream().map(Person::getAge).reduce(0, (a, b) -> a + b);
// 计算年龄总和
int sum = personList.stream().map(Person::getAge).reduce(0, Integer::sum);

// 价格使用BigDecimal防止精度丢失,将所有商品的价格累加
BigDecimal totalPrice = goodList.stream().map(GoodsCode::getPrice).reduce(BigDecimal.ZERO, BigDecimal::add);

11、anyMatch(T -> boolean表达式)

  • 流中是否有元素满足这个Boolean表达式
// 集合中是否存在一个元素的age等于500
boolean b = userList.stream().anyMatch(u-> u.getAge() == 500);

12、allMatch(T -> boolean) 和 noneMatch(T -> boolean)

  • allMatch(T -> boolean),即流中所有元素是否都满足boolean表达式。
  • noneMatch(T -> boolean),即是否流中没有一个元素满足boolean表达式。

可以配合filter一起使用。

示例:下面示例中准备了一个tagList集合中有标签A、B、C,3个元素,还有一个tagMap,其中key是序号,value是标签集合,现想获取tagMap中对应标签集合(value)和tagList集合完全不匹配的元素对应的序号(key)。

List<String> tagList = new ArrayList<>();
tagList.add("A");
tagList.add("B");
tagList.add("C");

Map<Integer, List<String>> tagMap = new HashMap<>();
List<String> list1 = new ArrayList<>();
list1.add("A");
list1.add("D");
List<String> list2 = new ArrayList<>();
list2.add("B");
List<String> list3 = new ArrayList<>();
list3.add("D");
tagMap.put(1, list1);
tagMap.put(2, list2);
tagMap.put(3, list3);
// 返回结果:[3]
List<Integer> collect = map.entrySet().stream().filter(entry -> list.stream().noneMatch(s -> entry.getValue().contains(s))).map(Map.Entry::getKey).collect(Collectors.toList());

13、count()

  • 返回流中元素的个数,返回long型。
int countOfAdult=persons.stream()
                   .filter(p -> p.getAge() > 18).map(person -> new Adult(person))
                       .count();

14、forEach()

  • 普通for循环或者增强for循环,break跳出整个循环,continue结束本次循环。Stream的forEach处理集合时需要使用关键字return跳出本次循环,并执行下次遍历(不能跳出整个流的forEach循环)。

它接受一个消费函数作为参数,该函数将被应用于流中的每个元素。

// 示例 1:打印流中的所有元素
letterList.stream().map(String::toUpperCase).forEach(System.out::println);

// 示例 2:将字符串流中的所有元素转换为大写并打印
letterList.stream().map(String::toUpperCase).forEach(System.out::println);

forEach()操作是终端操作之一,它不能被用于中间操作。也就是说,调用forEach()后,你不能再对流进行其他操作。另外,forEach()操作通常用于打印或其他副作用,而不是构建新的流或集合。

15、peek()

  • peek() 是一个中间操作方法,它允许你在不影响流的主要处理逻辑的情况下,查看或使用流中的每个元素。这个方法可以用来进行一些调试或日志记录等操作。

peek() 方法的签名如下:Stream<T> peek(Consumer<? super T> action)

其中,action 是一个 Consumer 函数,用于对流中的每个元素进行操作。这个函数不会改变流中的元素,也不会返回任何值。

以下是一个简单的示例,演示如何使用 peek() 方法来打印流中的每个元素:

List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");

fruits.stream()
    .filter(f -> f.length() > 5)
    .peek(System.out::println)
    .collect(Collectors.toList());

在这个例子中,我们首先创建了一个包含四个水果的列表。然后,使用 stream() 方法将其转换为一个流。接着,调用 filter() 方法来过滤出长度大于 5 的水果。然后,使用 peek() 方法在处理流中的元素时打印每个元素。最后,使用 collect() 方法将剩余的元素收集到一个新的列表中。

注意事项:

  • peek()不会改变流的结果:无论你在 peek() 方法中做了什么操作,流的最终结果都不会受到影响。
  • peek()可能会被无限次调用:如果你在流的中间操作中使用了 peek(),那么在每次中间操作时,peek() 都会被调用。
  • peek()是非短路操作:与 forEach() 不同,peek() 不是终端操作,它不会使流处理短路。也就是说,所有的中间操作都会被执行完毕,包括 peek()。
  • peek()可能会影响流的性能:如果在 peek() 方法中执行了非常耗时的操作,那么可能会影响流的整体性能。

peek() 主要适用于以下场景:

  • 调试:可以用来打印流中的元素,帮助你理解流的处理过程。
  • 日志记录:可以用来记录流中的元素,例如在处理大数据集时,记录每个处理的元素。
  • 副作用操作:可以用来执行一些副作用操作,例如更新数据库或发送通知等。

需要注意的是,虽然 peek() 提供了一个方便的方式来查看流中的元素,但它不应该被用于实际的业务逻辑中。因为它的主要目的是为了调试和日志记录,而不是处理流的主要逻辑。

16、Stream.iterate

java.util.stream.Stream下共有两个iterate,都是 Java 8 中引入的 Stream API 方法,用于生成无限流。它们的主要区别在于第二个方法允许你指定一个条件来决定何时停止生成元素。

方法一:

iterate(T seed, final UnaryOperator<T> f)

这个方法接受两个参数,会不断地应用函数 f 到前一个元素上,生成一个无限流。

  • seed:流的初始元素。
  • f:一个函数,用于将前一个元素转换为下一个元素。

例如,我们可以使用这个方法来生成自然数流:

// 这将输出从 1 到 10 的自然数。
Stream.iterate(1, x -> x + 1).limit(10).forEach(System.out::println);

方法二:

iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T
上一篇:FreeRTOS学习笔记1


下一篇:【Linux进程信号】Linux信号机制深度解析:保存与处理技巧-????1. 信号的保存