Java中的Stream流

文章目录

     前言
     第一步:创建流
     第二步:中间操作
     第三步:终结操作
     注意事项
     参考资料


使用 Stream 通常需要三步:创建流中间操作终结操作。在使用Stream(函数式编程)的时候,通常需要先行了解匿名函数和Lambda 表达式,整体框架如下图所示。
在这里插入图片描述

在IDEA中,可以使用快捷键 Alt + Enter 将Stream流快速进行简写或者从简写格式恢复成完整内容。

前言

在介绍 stream 流使用之前,首先需要介绍 Optional 类,它是 Java8 新引进的一个主要用于解决空指针异常的类。开发人员编写代码过程中经常会遇到空指针异常,因此很多时候需要进行各种非空判断,如以下代码。

Author author = getAuthor();
if (author != null) {
	System.out.println(author.getName());
}

尤其引用的还是对象类型时,这种判断会格外的多。因此 JDK8 引入了 Optional 类,可以借助此类写出更优雅的代码来避免空指针异常。此外,在函数式编程中,也会经常用到 Optional,因此需要对此进行学习和掌握。

Optional类
创建对象
消费对象
获取对象
ofNullable
of
empty
ifPresent
get
orElseGet
orElseThrow

1. 创建 Optional

那么该如何使用 Optional 呢?整体来说,Optional 就好像是包装类,可以把具体数据封装在 Optional对象内部,然后使用 Optional 中提供的方法操作内部的数据,这样就可以很优雅的避免空指针异常。

推荐使用 Optional 中的静态方法 ofNullable() 来把数据封装成一个 Optional 对象,无论传入的参数是否为 null 都不用担心出问题。

Author author = getAuthor();
Optional<Author> op = Optional.ofNullable(author);

如果 可以确定一个对象不会是 null,则可以直接使用 Optional 中的静态方法 of() 来把数据封装成 Optional 对象。

Author author = getAuthor();
Optional<Author> op = Optional.of(author);

如果 可以确定一个对象就是为 null,这个时候需要把 null 封装成 Optional 对象进行返回时,则可以使用 Optional 中的静态方法 empty 来进行封装。

Author author = getAuthor();
return author == null ? Optional.empty() : Optional.of(author);

2. 消费 Optional

获取到一个 Optional 对象后,可以使用 ifPresent() 方法使用该对象中存储的数据。此方法会判断对象内封装的数据是否为 null,只有不为 null 时才会执行具体的消费代码,因此使用起来更为安全。

Author author = getAuthor();
Optional<Author> op = Optional.ofNullable(author);
op.ifPresent(author -> System.out.println(author.getName()));

3. 获取 Optional

常用的方法有 get()orElseGet()orElseThrow() 等,其中不推荐 get 函数,因为如果封装的数据为 null 时,则会直接抛出异常,因此无法安全获取数据。

  • orElseGet():如果获取的数据为 null,则会返回初始设置的默认值【推荐】
Author author = getAuthor();
Optional<Author> op = Optional.ofNullable(author);
Author author1 = op.orElseGet(() -> new Author());
  • orElseThrow():如果获取的数据为 null,则会抛出异常【推荐】
Author author = getAuthor();
Optional<Author> op = Optional.ofNullable(author);
try {
	Author author1 = op.orElseThrow((Supplier<Throwable>) () -> new RuntimeException("author为空"));
	System.out.println(author.getName());
} catch (Throwable throwable) {
	throwable.printStackTrace();
}
Author author1 = op.orElseThrow(() -> new Author());

总结

使用Optional最佳实践思路就是先用 ofNullable 进行封装,然后使用 ifPresent 进行消费。

Optional<Author> op = Optional.ofNullable(getAuthor());
op.ifPresent(author -> System.out.println(author.getName));

第一步:创建流

1️⃣ 数组:Arrays.stream(数组) 或者使用 Stream.of(数组) 来创建

	Integer[] arr = {1,2,3,4,5};
	Stream<Integer> stream1 = Arrays.stream(arr);
	Stream<Integer> stream2 = Stram.of(arr);

📢 需要注意的是,Stream.of() 只能传入引用类型数组,如果传入基本数据类型数组( 如 int[]),则会报错。

2️⃣ 单列集合:集合对象.stream() (Collection:List接口、Set接口、Queue接口等)

	List<Author> authors = getAuthors();
	Stream<Author> stream = authors.stream();

3️⃣ 双列集合:转化为单列集合后,再创建Stream流 (Map:HashMap类、Hashtable类、TreeMap类等)

	Map<String, Integer> map = new HashMap<>();
	map.put("A", 1);
	map.put("B", 2);
	Stream<Map.Entry<String, Integer>> stream = map.entrySet().stream();

第二步:中间操作

🖐️ filter(过滤):可以对流中的元素进行条件过滤,符合过滤条件的才能继续留在流中。

// 打印所有年龄超过18岁的作家的姓名
List<Author> authors = getAuthors();
authors.stream()
	   .filter(author -> author.getAge() > 18)
	   .forEach(author -> System.out.println(author.getName()));

🖐️ map(映射):可以将流中的元素进行计算或者转换(一对一)。

// 打印所有作家的姓名
authors.stream()
	   .map(author -> author.getName())
	   .forEach(name -> System.out.println(name));

🖐️ flatMap(展开):可以将流中的元素进行计算或者转换(一对多)。

map 只能把一个对象转换成另一个对象来作为流中的元素,而 flatMap 可以把一个对象转换成多个对象作为流中的元素。

例子一:打印所有书籍的名称,要求对重复元素进行去重

authors.stream()
	   .flatMap(new Function<Author, Stream<Book>>() {
			@Override
			public Stream<Book> apply(Author author) {
				return author.getBooks().stream();
			}
		})
	   // .flatMap(author -> author.getBooks().stream()) // 简写版
	   .distinct()
	   .forEach(book -> System.out.println(book.getName()));

例子二:打印现有数据的所有分类,要求对分类进行去重

authors.stream()
	   .flatMap(author -> author.getBooks().stream())
	   .distinct()
	   .flatMap(book -> Arrays.stream(book.getCategory().split(",")))
	   .distinct()
	   .forEach(category -> System.out.println(category));

🖐️ distinct(去重):可以去除流中的重复元素。

// 打印所有作家的姓名,并且要求其中不能有重复元素
authors.stream()
	   .distinct()
	   .forEach(name -> System.out.println(name));

📢 需要注意的是,distinct() 方法是依赖 Object 中的 equals() 方法来判断是否为相同元素,所以一般需要重写 equals() 方法。

🖐️ sorted(排序):可以对流中的元素进行排序。

// 对流中的元素按照年龄进行降序排序,并且要求其中不能有重复元素
// 方式一:空参调用
authors.stream()
	   .distinct()
	   .sorted()
	   .forEach(author -> System.out.println(author.getAge()));
// 方式二:构造排序
authors.stream()
	   .distinct()
	   .sorted((o1, o2) -> o2.getAge() - o1.getAge())
	   .forEach(author -> System.out.println(author.getAge()));

📢 如果调用空参的 sorted() 方法时,需要流中的元素实现了 Comparable 接口,并重写 compareTo() 方法。

🖐️ limit(截取):可以设置流的最大长度,超出的部分将被抛弃。

// 对流中的元素按照年龄进行降序排序,并且要求不能有重复元素,然后打印其中年龄最大的两个作家的名称
authors.stream()
	   .distinct()
	   .sorted((o1, o2) -> o2.getAge() - o1.getAge())
	   .limit(2)
	   .forEach(author -> System.out.println(author.getName()));

🖐️ skip(偏移):跳过流中的前 n 个元素,返回剩下的元素。

// 打印除了年龄最大的作家外的其它作家,要求不能有重复元素,并且按照年龄降序进行排序
authors.stream()
	   .distinct()
	   .sorted((o1, o2) -> o2.getAge() - o1.getAge())
	   .skip(1)
	   .forEach(author -> System.out.println(author.getName()));

第三步:终结操作

🖐️ forEach(输出):对流中的元素进行遍历。

// 打印所有作家的名字
authors.stream()
	   .forEach(author -> System.out.println(author.getName()));

🖐️ count(总数):获取当前流中元素的个数。

// 打印这些作家所出书籍的数目,注意删除重复元素
long num = authors.stream()
	              .flatMap(author -> author.getBooks().stream())
	              .distinct()
	              .count();
System.out.println(num);

🖐️ min & max(最值):获取当前流中元素的最值。

底层其实是调用reduce方法进行实现的

// 获取这些作家所有书籍的最高分(或者最低分),并进行打印
Optional<Integer> op = authors.stream()
	                          .flatMap(author -> author.getBooks().stream())
	                          .map(book -> book.getScore())
	                          .max((score1, score2) -> score1 - score2);
System.out.println(op.get());

🖐️ collect(收集):把当前流转化成一个集合。

例子一:获取一个存放所有作者姓名的List集合

List<String> authorNames = authors.stream()
	                              .map(author -> author.getName())
	                              .collect(Collectors.toList());
System.out.println(authorNames);

例子二:获取一个存放所有书名的Set集合

Set<String> bookNames = authors.stream()
	                           .flatMap(author -> author.getBooks().stream())
	                           .map(book -> book.getName())
	                           .collect(Collectors.toSet());
System.out.println(bookNames);

例子三:获取一个Map集合,其中key为作者名,value为List<Book>

Map<String, List<Book>> map = authors.stream()
								     .distinct() // 因为 Map 中的 key 不能重复,因此需要去重操作
	                                 .collect(Collectors.toMap(author -> author.getName(), author -> author.getBooks()));
	                                 // .collect(Collectors.toMap(author::getName, author::getBooks)) // 精简版
System.out.println(names);

🔜 终结操作中的查找与匹配

  • anyMatch:用来判断是否有任意符合匹配条件的元素(返回值为 boolean 类型)
// 判断是否有年龄超过29的作家
boolean flag = authors.stream()
				      .anyMatch(author -> author.getAge() > 29);
System.out.println(flag);
  • allMatch:用来判断是否都符合匹配条件,如果都符合则返回 true,否则返回 false
// 判断是否所有的作家都是成年人
boolean flag = authors.stream()
				      .allMatch(author -> author.getAge() >= 18);
System.out.println(flag);
  • noneMatch:用来判断是否都不符合匹配条件,如果都不符合则返回 true,否则返回 false
// 判断作家是否都没有超过100岁
boolean flag = authors.stream()
				      .noneMatch(author -> author.getAge() > 100);
System.out.println(flag);
  • findAny:获取流中的任意一个元素(无法保证获取的一定是流中的第一个元素)
// 获取任意一个大于18岁的作家,如果存在则输出ta的姓名
Option<Author> op = authors.stream()
				           .filter(author -> author.getAge() > 18)
				           .findAny();
op.ifPresent(author -> System.out.println(author.getName()));
  • findFitst:获取流中的第一个元素
// 获取一个年龄最小的作家,并输出ta的姓名
Option<Author> op = authors.stream()
				           .sorted((o1, o2) -> o1.getAge() - o2.getAge())
				           .findFirst();
op.ifPresent(author -> System.out.println(author.getName()));

🔜 终结操作中的归并

🖐️ reduce:对流中的所有元素进行计算,并将计算结果进行返回 (缩减操作)

reduce 的作用是把 stream 中的元素组合起来,通过传入一个初始值,按照规定的计算方式依次将流中的元素和初始值进行计算,计算结果再和后面的元素进行计算,reduce 两个参数的重载形式内部计算流程如下所示。

// identity:初始值
// apply:可以自定义计算流程
T result = identity;
for (T element : this stream) {
	result = accumulator.apply(result, element);
}
return result;

📢 其实还有一个参数的重载函数(源码已给出说明)。与上述流程不同点在于,此方式是把流中遍历到的 第一个元素 作为 result 的 初始值,其它逻辑大同小异,具体实现流程如下所示。

boolean foundAny = false;
T result = null;
for (T element : this stream) {
	if (!foundAny) {
		foundAny = true;
		result = element;
	}
	else
		result = accumulator.apply(result, element);
}
return foundAny ? Optional.of(result) : Optional.empty();

综上所示,如果要求指定初始值时,需要使用两个参数的重载形式,否则使用一个参数的重载形式即可,默认将第一个元素作为初始值。本文更推荐使用手动赋予初始值,这样可增强代码的可读性。

例子一:使用reduce计算所有作者年龄之和

Integer sum = authors.stream()
	                 .map(author -> author.getAge())
	                 .reduce(0, (result, element) -> result + element);
System.out.println(sum);

例子二:使用reduce计算所有作者年龄的最大值

Integer maxAge = authors.stream()
	                    .map(author -> author.getAge())
	                    .reduce(Integer.MIN_VALUE, (result, element) -> result < element ? element : result); // 这里的第一个参数 Integer.MIN_VALUE 其实可以省略
System.out.println(maxAge);

例子三:使用reduce计算所有作者年龄的最小值

Integer minAge = authors.stream()
	                    .map(author -> author.getAge())
	                    .reduce(Integer.MAX_VALUE, (result, element) -> result < element ? result : element); // 这里的第一个参数 Integer.MAX_VALUE 其实可以省略
System.out.println(minAge);

注意事项

🔊 惰性求值:如果只有中间操作,而不添加终结操作,则当前流不会执行。
🔉 单次使用:一旦流对象完成终结操作之后,则无法被再次使用。
🔈 影子模式:无论对流进行怎样处理,均不影响原始集合中的值。

在正常情况下,stream 会遵守影子模式,但是仍可违背此模式,如以下即为破坏影子模式的示例代码:

authors.stream()
	   .map(author -> {
			author.setAge(author.getAge() + 10); // 强行修改原集合中的值
			return author;
	   })...

🔜 高阶用法

🔊 [基础数据类型优化]:由于 Stream 流在很多情况下都使用了泛型,导致参数和返回值大都是引用类型。如果操作的是基础数据类型(如整型等),那么在大量数据的情况下会频繁进行自动拆箱和自动装箱,示例代码如下所示。

List<Author> authors = getAuthors();
authors.stream()
	   .map(author -> author.getAge()) // 这里的数据类型为Integer
	   .map(age -> age + 10)
	   .filter(age -> age > 18)
	   .map(age -> age + 2)
	   .forEach(System.out::println);

在上述代码中,三个 map 函数均会进行自动装箱和拆箱,会造成一定的时间消耗,因此 Stream 提供了针对基础数据类型的方法,将上述代码优化效果如下:

List<Author> authors = getAuthors();
authors.stream()
	   .mapToInt(author -> author.getAge()) // 这里的数据类型被转换为int,后续基于int进行相应处理
	   .map(age -> age + 10)
	   .filter(age -> age > 18)
	   .map(age -> age + 2)
	   .forEach(System.out::println);

除了上述 mapToInt,还有诸如 mapToLongmapToDoubleflatMapToIntflatMapToDouble 等可以使用。

🔊 [并行流]:当 Stream 流存在大量元素,可以使用并行流来提高处理效率,其本质上是将任务分配给多个线程去完成。我们也可以自行实现对应逻辑,但是需要对并发编程有足够的了解,而 Stream 已经提供了对应的方法,因此可以很简便的完成此操作,示例代码如下所示。

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
int sum = stream.parallel() // 这里将串行流设置为并行流
				.peek(num -> System.out.println(num + Thread.currentThread().getName())) // Stream 专门用来调试并行流的接口(也可用于串行流)
				.filter(num -> num > 5)
	       		.reduce(0, (result, element) -> result + element)
	     		.intValue();
System.out.println(sum);

在上述代码中,通过 parallel 方法可以将串行流转换为并行流。此外,也可以直接通过 parallelStream 方法获取并行流对象。

List<Author> authors = getAuthors();
authors.parallelStream()
	   .map(author -> author.getAge())
	   . // do something

参考资料

Optional使用示例 · CSDN
Stream · 菜鸟教程
Lambda表达式 & Stream流 · B站

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值