文章目录
前言
第一步:创建流
第二步:中间操作
第三步:终结操作
注意事项
参考资料
使用 Stream 通常需要三步:创建流
、中间操作
、终结操作
。在使用Stream(函数式编程)的时候,通常需要先行了解匿名函数和Lambda 表达式,整体框架如下图所示。
在IDEA中,可以使用快捷键 Alt + Enter
将Stream流快速进行简写或者从简写格式恢复成完整内容。
前言
在介绍 stream 流使用之前,首先需要介绍 Optional
类,它是 Java8 新引进的一个主要用于解决空指针异常的类。开发人员编写代码过程中经常会遇到空指针异常,因此很多时候需要进行各种非空判断,如以下代码。
Author author = getAuthor();
if (author != null) {
System.out.println(author.getName());
}
尤其引用的还是对象类型时,这种判断会格外的多。因此 JDK8 引入了 Optional 类,可以借助此类写出更优雅的代码来避免空指针异常。此外,在函数式编程中,也会经常用到 Optional,因此需要对此进行学习和掌握。
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
,还有诸如 mapToLong
、mapToDouble
、flatMapToInt
、flatMapToDouble
等可以使用。
🔊 [并行流]
:当 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