巧用Stream流
介绍
JDK1.8 引入了许多新特性,其中就包括 Lambda
表达式和 Stream
流,Lambda
表达式允许将方法作为参数,极大简化了方法和接口的实现。不过我们今天的重点不是这个,而是 Stream
流。
一、前言
- 流是什么
- 流的特性
- 流与集合
1.1、流是什么
Stream 流是一个泛型接口,继承自 BaseStream
,实现了 AutoCloseable
接口。以下图片展示了 Stream 流的类继承结构:
分析 Stream 这个单词,它有“ 流动; 溪流 ”的意思。恰巧,流的作用也与此相关,它主要用于处理序列元素。流中的序列元素就像水流一样,从 “ 源数据” 开始,经过中间操作的 “ 处理 ”,最后到达 “ 目的地 ”(指结束操作)。
流支持顺序或并行的执行模式,以下示例代码演示了使用 Stream
和 IntStream
流计算总和的操作:
int sum = widgets.stream()
.filter(w -> w.getColor == RED)
.mapToInt(w -> w.getWeight())
.sum();
以上示例中,widgets
是一个集合。通过 Collection.stream()
方法创建 Widget
类型对象流,过滤器产生仅包含红色的 Widgets 的流,然后将每个红色的 widget 对象的重量转为 int 整型值的 stream
流,最后计算总和。
除了上面说到的 Stream 流,IntStream
、LongStream
和 DoubleStream
则是指定类型的流,所有的这些都可以作为 “ 流 ” 进行使用并且符合其特征和描述限制。
为了执行计算,Stream 流操作会生成一个管道,这些管道由源数据构成,可能包含 0 个或多个中间操作, 以及一个必要的结束操作。此外,流在处理时会进行懒加载,源数据的运算仅当结束操作初始化后才执行,并且源数据仅在必要时使用。
小提示
- 数组、集合、生成器函数或 I/O 通道可以作为流的源数据
- 流的中间操作可能会创建一个新的流对象,而流的结束操作会生成一个 结果 或 “ 副产物 ”。
1.2、流的特性
前一章简单地介绍了一下 Stream 流,下面接着讲讲流的一些特性,这部分很重要,要记好小本本
流的操作不能随意修改源数据
如上面的 “ widgets ” 示例的流管道,可以被视为对流元素的查询。除非元素是明确设计可以用于并发修改的 (例如 ConcurrentHashMap
类),否则在查询源数据的同时修改它可能会导致不可预料或错误的行为。
流的参数必须为函数式接口的实例
大多数的流操作接受正确描述用户指定行为的参数, 例如,1.1、流是什么 中的示例 lambda 表达式 w -> w.getWeight()
将重量映射为整数。为了保持正确的行为,这些行为的参数:
- 不能影响到流的源数据
- 而且,其结果不能依赖于任何可能会改变执行中数据的行为
这样的参数一定是函数式接口的实例(类似于 Function
接口),而且其表现形式通常为 lambda 表达式或方法引用 。此外,除非另有说明,否则其参数必须非空。
单个流实例最多只能操作一次
每个流只能执行最多一次的操作。假如有相同的源数据提供了两个或多个流管道,或进行了多次遍历,我们的应用程序又刚好检查到这种重复使用的情况,其实现就可能会抛出 IllegalStateException
异常。 毫无疑问地说,这种行为可以排除 “ 分叉 ” 流。然而,由于某些流操作可能会返回其他对象而不是一个新的流对象,因此系统不可能排查所有的情况。
大部分的流实例无需进行资源管理
虽然流有一个实现了 AutoCloseable
接口的 close()
方法,但是近乎所有的流实例并不需要在使用后进行关闭。一般来说,如果流的唯一源数据是 I/O 通道,例如 Files.lines(Path, Charset)
方法返回的这些,那就需要进行关闭。因为大多数流仅通过集合、数组或生成函数返回,所以不需要特殊的资源管理。
流支持顺序或并行的执行模式
流的操作可以按顺序,或是按并行的方式执行,这种执行模式是流的属性。这种属性通过初始化顺序或并行执行的选择来创建。例如,Collection.stream()
方法可以用于创建顺序流,Collection.parallelStream()
则用于创建并行流。此外,还可以通过流本身的 sequential()
或 parallel()
方法修改流的执行模式。
冷知识
- 如果流需要关闭,可以在
try-with-resources
语句中将其声明为资源- 可以通过
isParallel()
方法查询流是否为并行流
1.3、流与集合
说到 Stream 流,就不得不提到集合,它们表面上虽然有一些相似的地方,但是所追求的目标是不一样的:
Collection | Stream | |
---|---|---|
直接操作(访问,移除或新增元素等等) | 支持 | 不支持 |
计算操作(最大最小值,元素去重等等) | 不支持 | 支持 |
关注点 | 序列元素的高效管理和访问 | 声明式描述其序列元素以及将会在序列元素上聚合执行的计算操作 |
如上表所示,虽然集合本身不支持计算操作,但是我们可以借助循环手动实现对集合的计算操作,为什么还要使用 Stream 流的方法?。原因的话,无非有以下两点:
- lambda 表达式或方法引用的形式,简化了计算的操作实现,提高了代码的可读性
- 流的实现执行起来通常要比使用循环实现的等价代码要快很多
那么,同样的计算操作,流是否真的比集合更好用更快呢?下面就让我们来看看:
/**
* Stream 流方法和集合通过循环手动的计算操作的性能比较
* 数据类型:Integer 整型
* 测试数据:随机
* 计算操作:转换Double、求最大值、去重、过滤指定条件的元素、
*
* @author wl
* @date 2022/9/18 1:31
*/
public class CapacityTest {
/** 指定的数组长度 */
private static final int ARR_LENGTH = 1_0000;
public static void main(String[] args) {
// 初始化测试数据
Random random = new Random();
Integer[] arr = new Integer[ARR_LENGTH];
for (int i = 0; i < arr.length; i++) {
arr[i] = random.nextInt(1000) + 1;
}
System.out.println("测试数据初始化完成,准备进行性能比较");
// 将测试数据转换为集合和流
List<Integer> list = new ArrayList<>(Arrays.asList(arr));
// 进行性能比较
System.out.println("-------------------------------------------------");
System.out.println("| 操作 \\ 类型 | 集 合\t\t|\tStream 流\t|");
for (int i = 0; i < 4; i++) {
String operate = "";
switch (i) {
case 0:
operate = "转换类型";
break;
case 1:
operate = "求最大值";
break;
case 2:
operate = "列表去重";
break;
case 3:
operate = "元素过滤";
break;
}
long collectionConsumeTime = TestCollection(list, i);
long streamConsumeTime = TestStream(list.stream(), i);
System.out.println("-------------------------------------------------");
if(i == 0) {
if(ARR_LENGTH >= 800_0000) {
System.out.println("| " + operate + " | " + collectionConsumeTime + "\t|\t\t" + streamConsumeTime +"\t|");
} else if(ARR_LENGTH >= 700_0000 && ARR_LENGTH < 800_0000) {
System.out.println("| " + operate + " | " + collectionConsumeTime + "\t|\t\t" + streamConsumeTime +"\t\t|");
} else if(ARR_LENGTH >= 600_0000 && ARR_LENGTH < 700_0000) {
System.out.println("| " + operate + " | " + collectionConsumeTime + "\t|\t\t" + streamConsumeTime +"\t|");
} else if(ARR_LENGTH >= 500_0000 && ARR_LENGTH < 600_0000) {
System.out.println("| " + operate + " | " + collectionConsumeTime + "\t|\t\t" + streamConsumeTime +"\t\t|");
} else if(ARR_LENGTH >= 300_0000 && ARR_LENGTH < 500_0000) {
System.out.println("| " + operate + " | " + collectionConsumeTime + "\t\t|\t\t" + streamConsumeTime +"\t|");
} else {
System.out.println("| " + operate + " | " + collectionConsumeTime + "\t\t|\t\t" + streamConsumeTime +"\t\t|");
}
} else {
System.out.println("| " + operate + " | " + collectionConsumeTime + "\t\t|\t\t" + streamConsumeTime +"\t\t|");
}
}
System.out.println("-------------------------------------------------");
}
/**
* 测试集合通过循环手动实现
*
* @param operate 计算操作
* @author wl
* @date 2022/9/18 1:46
* @return long
*/
public static long TestCollection(List<Integer> list, int operate) {
long startTime = System.currentTimeMillis();
switch (operate) {
// 转换Double
case 0:
List<Double> doubleList = new ArrayList<>(list.size());
for (int i = 0; i < list.size(); i++) {
Integer num = list.get(i);
doubleList.add(num.doubleValue());
}
break;
// 求最大值
case 1:
Integer max = list.get(0);
for (int i = 0; i < list.size(); i++) {
Integer num = list.get(i);
if(num > max) {
max = num;
}
}
break;
// 去重(通过Set间接实现)
case 2:
Set<Integer> set = new HashSet<>(list.size());
List<Integer> distinctList = new ArrayList<>(list.size());
for (int i = 0; i < list.size(); i++) {
Integer num = list.get(i);
boolean flag = set.add(num);
if(flag) {
distinctList.add(num);
}
}
list = distinctList;
break;
// 过滤指定条件的元素
case 3:
List<Integer> filterList = new ArrayList<>(list.size());
for (int i = 0; i < list.size(); i++) {
Integer num = list.get(i);
if(!num.equals(100)) {
filterList.add(num);
}
}
list = filterList;
break;
}
return System.currentTimeMillis() - startTime;
}
/**
* 测试 Stream 流方法实现
*
* @param operate 计算操作
* @author wl
* @date 2022/9/18 1:46
* @return long
*/
public static long TestStream(Stream<Integer> stream, int operate) {
long startTime = System.currentTimeMillis();
switch (operate) {
// 转换Double
case 0:
List<Double> doubleList = stream.map(Integer::doubleValue).collect(Collectors.toList());
break;
// 求最大值
case 1:
Integer max = stream.max(Integer::compareTo).get();
break;
// 去重
case 2:
List<Integer> distinctList = stream.distinct().collect(Collectors.toList());
break;
// 过滤指定条件的元素
case 3:
List<Integer> filterList = stream.filter(i -> !i.equals(100)).collect(Collectors.toList());
break;
}
return System.currentTimeMillis() - startTime;
}
}
当数据量为十万时:
测试数据初始化完成,准备进行性能比较
-------------------------------------------------
| 操作 \ 类型 | 集 合 | Stream 流 |
-------------------------------------------------
| 转换类型 | 9 | 70 |
-------------------------------------------------
| 求最大值 | 3 | 3 |
-------------------------------------------------
| 列表去重 | 6 | 4 |
-------------------------------------------------
| 元素过滤 | 2 | 3 |
-------------------------------------------------
当数据量为五百万时:
测试数据初始化完成,准备进行性能比较
-------------------------------------------------
| 操作 \ 类型 | 集 合 | Stream 流 |
-------------------------------------------------
| 转换类型 | 2208 | 208 |
-------------------------------------------------
| 求最大值 | 45 | 32 |
-------------------------------------------------
| 列表去重 | 66 | 66 |
-------------------------------------------------
| 元素过滤 | 103 | 194 |
-------------------------------------------------
当数据量为两千万时:
测试数据初始化完成,准备进行性能比较
-------------------------------------------------
| 操作 \ 类型 | 集 合 | Stream 流 |
-------------------------------------------------
| 转换类型 | 9083 | 1645 |
-------------------------------------------------
| 求最大值 | 156 | 83 |
-------------------------------------------------
| 列表去重 | 245 | 263 |
-------------------------------------------------
| 元素过滤 | 402 | 183 |
-------------------------------------------------
可以看到在 Stream 流方法在处理一些复杂操作时效率比集合会高出很多,数据量越大这种特征越明显。此外,如果提供的元素序列本身是有序的话,那么在进行去重、排序等操作时,其结果是稳定的。
二、流的创建
- 自带方法
- 其他方式
2.1、自带方法
流自身提供了很多方法用于创建,下面我们就来看看都有哪些:
通用方法
empty()
方法
此方法返回一个空的流实例对象,其返回的元素类型是 Object,如果你需要空数据的流,那么可以试试这个方法,示例代码如下:// 返回一个 Object 类型的空流对象 Stream<Object> empty = Stream.empty();
of(T t)
方法(常用)
此方法返回包含单个元素的流实例对象,当我们只有一个元素时可以使用此方法。可以使用此方法将单个元素转为列表,但是不如直接使用Collections.singletonList()
来的方便,示例代码如下:// 使用给定的单个元素创建流 Stream<Long> longStream = Stream.of(1L);
of(T... values)
方法(常用)
此方法返回的流元素都是指定的值,其参数 values 为可变形参,可以传递任意数量的参数,也可以使用数组作为参数,示例代码如下:// 可以放置任意数量的参数用于创建流 Stream<Double> doubleStream = Stream.of(1.4D, 2.8D, 3.9D, 7.2D); // 也可以将数组作为参数来创建流对象 int[] arr = {1,2,3,4,5}; Stream<int[]> stream = Stream.of(arr);
当我们想使用数组创建流时可以使用此方法,需要注意的是,当把基本数据类型的数组作为参数进行传递时,泛型的类型会是指定的数组类型,而不是对应的包装类
iterate(final T seed, final UnaryOperator<T> f)
方法
此方法通过使用指定的 seed 种子初始化指定的 f 函数并进行迭代,生成并返回一个无限元素的流,通常需要配合limit()
方法进行使用。此方法常用于生成指定规律的数据序列,例如返回偶数列,奇数列等等。示例代码如下:/* 生成 n + 1 规律的数据序列, n 从 2 开始 返回的数据相当于 3、4、5、6... */ Stream<Integer> iterate = Stream.iterate(2, n -> n + 1); List<Integer> collect = iterate // 返回前十条数据 .limit(10) // 转换为集合并返回 .collect(Collectors.toList());
generate(Supplier<T> s)
方法
此方法返回无限元素的流,需要配合limit()
方法使用,每个元素通过指定的函数生成,适用于生成常量流,随机数流。示例代码如下:Random random = new Random(); List<Integer> integerList = Stream // 生成任意个 1-1000 内的随机数 .generate(() -> random.nextInt(1000) + 1) // 返回前十条数据 .limit(10) // 转换为集合并返回 .collect(Collectors.toList());
concat(Stream<? extends T> a, Stream<? extends T> b)
方法(常用)
此方法可以用于合并流,此流的元素首先是第一个流的所有元素,然后是第二个流的所有元素。如果输入的两个流是有序的则结果流是有序的,如果任意一个输入流是并行的,则返回的结果流是并行的。当结果流关闭时,系统会同时关闭两个流,示例代码如下:Stream<Integer> stream = Stream // 合并两个流的数据 .concat(Stream.of(1, 3, 7), Stream.of(2, 4, 6));
builder()
方法
使用此方法可以间接创建流,适用于想通过循环语句创建流的情况。它返回一个 Stream 流的 builder 类,通过此类的accept(t)
或add(t)
方法添加数据,最后使用build()
方法构造流实例对象,示例代码如下:// 获取建造者对象 Stream.Builder<Integer> builder = Stream.builder(); for (int i = 0; i < 4; i++) { // 循环添加元素 // 此方法无返回值 builder.accept(i); // 调用此方法会返回本身,可以通过代码链的形式使用此方法 // builder.add(i); } List<Integer> collect = builder // 使用添加的源数据建造流实例对象 .build() // 转换为集合并返回 .collect(Collectors.toList());
注意:在调用完 build() 方法之后,就不能对 builder 类执行任何其他的操作了,否则会抛出 IllegalStateException
异常
特殊方法
除了上述提到的几个方法外,IntStream、LongStream 流还有一些专用的方法可以用于创建流
range(int startInclusive, int endExclusive)
方法这个方法会根据指定的范围有序地创建元素数据,并且返回对应类型的流实例对象。需要注意的是,返回数据不包含上边界,代码示例如下:
IntStream // 有序地创建从 1开始直到 30-1 结束的元素数据 .range(1, 30) // 查看创建的数据 .peek(i -> System.out.println("before range: " + i)) // 转换为 ArrayList 类型的集合 .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
此方法相当于下面的 for 循环表达式:
for (int i = startInclusize; i < endExclusive; i++) {...}
rangeClosed(int startInclusive, int endInclusive)
方法这个方法会根据指定的范围有序地创建元素数据,并且返回对应类型的流实例对象。
和上面的 range() 方法的区别是:返回的数据包含上边界。
其实从参数名称我们也能猜出来区别在哪,代码示例如下:LongStream // 有序地创建从 2 开始直到 30 结束的元素数据 .rangeClosed(2L, 30L) // 查看创建的数据 .peek(i -> System.out.println("before range: " + i)) // 转换为 ArrayList 类型的集合 .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
此方法相当于下面的 for 循环表达式:
for (int i = startInclusize; i <= startInclusive; i++) {...}
此外,在使用上面的方法时要注意,如果起始索引比结束索引值大,会返回一个空流。
2.2、其他方式
- 数组
- 集合
- I/0 通道
2.2.1、数组
数组是 Java 语言中一类特殊的对象,由 JVM 直接创建。 如果想通过数组来创建 Stream 流,可以直接使用 JDK 提供的 Arrays
工具类,它拥有可以创建包括 IntStream、LongStream 流、DoubleStream 流在内的方法,此外,还支持截取指定范围内的源数据,然后再将其转化为流对象的功能。
创建 Stream 类型的流
Arrays.stream(T[] array)
方法(常用)
此方法仅适用于对象类型的数据,示例代码如下:
// 创建一个Integer 包装类的数组
Integer[] arr = {1, 2, 3, 4, };
// 通过 Integer 数组创建流实例
Stream<Integer> stream = Arrays.stream(arr);
Arrays.stream(T[] array, int startInclusive, int endExclusive)
方法(常用)
如果需要截取指定下标范围内的数据来创建流,那么可以使用此方法,它接受一个泛型数组,一个开始下标和一个结束下标,返回由指定下标内的数据锁组成的流实例对象。注意,如果开始下标小于0、或结束下标,又或者结束下标大于指定数组的下标范围,那么此方法将会抛出 ArrayIndexOutOfBoundsException
异常,示例代码如下:
// 创建一个 Double 类型的数组
Double[] arr = {1.2, -8.4, 1.1, 5.9, -37.2, 89.5, 74.1, 22.6, 3.1, };
// 使用 0 - 2 下标范围内的数据创建流对象并返回
Stream<Double> stream = Arrays.stream(arr, 0, 2);
创建其他类型的流
除了上面说的方法之外,如果您想使用其他类型的流对象,可以通过其基本数组类型数组进行创建,示例代码如下:
// 创建 IntStream:
int[] intArr = {1, 2, 3, 4, };
IntStream intStream = Arrays.stream(intArr);
IntStream intStream2 = Arrays.stream(intArr, 0, 2);
// 创建 DoubleStream:
double[] dobArr = {-1.2, 3.4, 1.0, 0.0, };
DoubleStream doubleStream = Arrays.stream(dobArr);
DoubleStream doubleStream2 = Arrays.stream(dobArr, 0, 2);
// 创建 LongStream:
long[] longArr = {0L, 7L, -3L, 9L, 34L, };
LongStream longStream = Arrays.stream(longArr);
LongStream longStream2 = Arrays.stream(longArr, 0, 2);
2.2.2、集合
使用集合创建流似乎比数组更简单,因为方法是集合自带的,而且不需要任何参数,不像数组还需要借助其工具类来创建。
Collection.stream()
方法(常用)
此方法将当前集合作为流的源数据并返回一个流实例,当我们想通过集合创建流时,经常会用到这个方法,示例代码如下:
// 创建一个集合
List<Integer> list = new ArrayList<>(0);
// 通过集合创建流实例
Stream<Integer> stream = list.stream();
Collection.parallelStream()
方法(常用)
此方法返回一个以当前集合作为流的源数据的并行流实例,这种执行模式下的流实例适合在多线程的情况下使用,注意,数据量大且列表元素本身也为多线程的情况下使用并行流是最好的,其他情况下误用可能导致线程不安全的情况发生。示例代码如下:
// 创建一个集合
List<Integer> list = new ArrayList<>(0);
// 通过集合创建并行流的实例
Stream<Integer> stream = list.parallelStream();
2.2.3、I/0 通道
如果想从文件或目录中获取流对象,Files 工具类是个不错的选择,它提供了很多便捷的方法用于创建流对象。此外,要注意的是,下面的所有方法都会抛出 I/O 异常,需要根据实际业务情况进行捕获或抛出处理。
Files.list(Path dir)
方法
此方法返回一个懒加载流,其中的元素是目录中的条目数据,且列表不是递归的。流的元素是根据 dir 解析目录条目的名称从而获取的 Path 对象。 一些文件系统维持目录本身以及目录的父目录的特殊链接,其中不包括代表这些链接的目录条目。
虽然流是弱一致性的,线程安全的,但在迭代时不会冻结目录,因此可能反射从该方法返回后发生的目录更新。
返回流封装了一个DirectoryStream
,如果文件系统资源的及时丢弃是必要的,那么使用try-with-resources
语句构造确保流操作已完成后调用流的 close 方法。
对关闭的流进行操作就像已经到达了流的末尾,由于预读,一个或多个元素可能在关闭流之后返回。
如果这个方法返回之后访问目录时抛出IOException
异常,则将其包裹在UncheckedIOException
异常,该异常将从导致访问发生的地方抛出。Path path = Paths.get(doPrivileged(new GetPropertyAction("java.io.tmpdir"))); Stream<Path> list = Files.list(path);
Files.walk(Path start, int maxDepth, FileVisitOption... options)
方法
此方法通过遍历以给定起始文件为根的文件树,返回一个 Path 懒加载流,文件树是深度优先遍历的,流中的元素是 Path 对象,就好像通过解析相对路径来获得的一样。
当消耗元素时遍历此流的文件树,返回的 Stream 流至少包含一个元素,即起始文件自身。对于每个访问的文件,流都会尝试读取其BasicFileAttributes
。 如果文件是目录并且可以成功打开,则目录中的条目及其后代将在邂逅时跟随流中的目录。当所有条目都被访问后,目录就关闭了, 然后在目录的下一个兄弟节点处文件树继续遍历。
虽然流是弱一致性的,线程安全的,但在迭代时不会冻结目录,因此可能反射从该方法返回后发生的目录更新。
默认情况下,符号链接不自动跟随这个方法,如果options
参数包含FOLLOW_LINKS
选项则符号链接会跟随。当跟随链接时如果无法读取目标属性,那么此方法尝试自动获取BasicFileAttributes
的链接。
如果options
参数包含FOLLOW_LINKS
选项,那么 Stream 流保持对目录的追踪访问便于循环能够检测。此外,当目录中有一个条目是该目录的祖先时,就会出现一个循环,此循环通过记录目录的文件唯一标识符进行检测,如果文件唯一标识符不可用,那就调用isSameFile()
方法测试目录与相同文件是否与祖先相同。当检测到循环时,它被视为带有FileSystemLoopException
异常实例的 I/O 错误。
maxDepth
参数是访问目录的最大层级数,除非安全管理器拒绝访问,否则零值表示仅访问起始文件,而Intger 最大值可以用于表示所有应该访问的层级。
当安装了安全管理器并且其拒绝访问文件或目录,那么在流中会它被忽视且不包括在内。
返回流封装了一个或多个DirectoryStream
,如果文件系统资源的及时丢弃是必要的,那么使用try-with-resources
语句构造确保流操作已完成后调用流的 close 方法。此外,关闭流操作会引发IllegalStateException
异常。
如果这个方法返回之后访问目录时抛出IOException
异常,则将其包裹在UncheckedIOException
异常,该异常将从导致访问发生的地方抛出,示例代码如下:Path path = Paths.get(doPrivileged(new GetPropertyAction("java.io.tmpdir"))); Stream<Path> list = Files.walk(path,1, FileVisitOption.FOLLOW_LINKS); list.forEach(System.out::println);
Files.walk(Path start, FileVisitOption... options)
方法
此方法通过遍历以给定起始文件为根的文件树,返回一个 Path 懒加载流,文件树是深度优先遍历的,流中的元素是 Path 对象,就好像通过解析相对路径来获得的一样。
这个方法的工作方式就像调用它的等价表达式:walk(start, Integer.MAX_VALUE, options)
换句话说,他会访问文件树的所有层级。
此方法的返回流封装了一个或多个DirectoryStream
,如果文件系统资源的及时丢弃是必要的,那么可以构造try-with-resources
语句确保流操作已完成后再调用流的 close 方法。此外,关闭流操作可能会引发IllegalStateException
异常,示例代码如下:
Path path = Paths.get(doPrivileged(new GetPropertyAction("java.io.tmpdir")));
Stream<Path> list = Files.walk(path, FileVisitOption.FOLLOW_LINKS);
Files.find(Path start, int maxDepth, BiPredicate<Path, BasicFileAttributes> matcher, FileVisitOption... options)
方法
通过遍历以给定起始文件为根的文件树,返回一个 Path 懒加载流。
此方法完全按照walk()
方法指定的方式遍历文件树,对于每个邂逅的文件,调用给定的BiPredicate
和其 Path 以及BasicFileAttributes
。 Path 对象是通过解析相对路径来获得的,如果 BiPredicate 返回 true,则仅包含在返回的 Stream 中。与 walk 方法返回的 Stream 上调用的过滤器相比,通过避免对BasicFileAttributes
的冗余检索,此方法可能更有效。
返回流封装了一个或多个DirectoryStream
,如果文件系统资源的及时丢弃是必要的,那么使用try-with-resources
语句构造确保流操作已完成后调用流的 close 方法。此外,关闭流操作会引发IllegalStateException
异常。
如果这个方法返回之后访问目录时抛出IOException
异常,则将其包裹在UncheckedIOException
异常,该异常将从导致访问发生的地方抛出,示例代码如下:Path path = Paths.get(doPrivileged(new GetPropertyAction("java.io.tmpdir"))); try(Stream<Path> pathStream = Files.find(path, 0, (a, b) -> { // 过滤指定条件的数据 boolean symbolicLink = b.isSymbolicLink(); int nameCount = a.getNameCount(); return !symbolicLink && nameCount > 0; }, FileVisitOption.FOLLOW_LINKS)) { List<Path> collect = pathStream.collect(Collectors.toList()); collect.forEach(i -> System.out.println("i = " + i)); } catch (IOException e) { e.printStackTrace(); }
Files.lines(Path path, Charset cs)
方法 (常用)
从文件中读入所有的行作为 Stream 流,和 readAllLines() 方法不同的是,这个方法不会将所有行读入列表,而是随着流的消耗进行懒加载。
使用指定的字符集将文件中的字节解码为字符并且支持 readAllLines() 方法指定的行终止符。
在此方法返回后,从文件读取或读取格式错误或不可映射的字节序列时发生的任何后续 I/O 异常都将包装在 UncheckedIOException 异常中,该异常将从导致读取发生的 Stream 方法中抛。 如果关闭文件时抛出 IOException 异常,它也会被包装为 UncheckedIOException 异常。如果 path 是文件的路径,那么下面的示例将生成该文件中包含的单词流:
Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8); Stream<String> words = lines.flatMap(line -> Stream.of(line.split(" +")));
Files.lines(Path path)
方法 (常用)
从文件中读取所有行作为 Stream 流,使用 UTF-8 的字符集将文件中的字节解码为字符。
这个方法的工作方式就像调用它的等价表达式:Files.lines(path, StandardCharsets.UTF_8)
。我们可以使用这个方法从文件读取数据并将其作为流进行处理,比如,从文件中读取单词、文本等等
如果 path 是文件的路径,那么下面的示例将生成该文件中包含的单词流:Stream<String> lines = Files.lines(path); Stream<String> words = lines.flatMap(line -> Stream.of(line.split(" +")));
冷知识
通过Stream
、集合和数组的方式创建Stream
流对象的五花八门,但是这些方式,底层不外乎其实都调用了StreamSupport
类的方法。
这个类有一些低级的工具方法用于创建和操纵Stream
流,它主要的目的是为库编写者呈现数据结构的流视图,大多数面向终端用户的静态流方法都在各种的Stream
类中,大家如果有兴趣的话,可以自行查看StreamSupport
类的源码,这里我不做详细说明。
三、流的使用
前言
Stream 流提供了很多方法用于处理复杂的代码逻辑,这极大简化了程序员的日常开发,提升了我们的工作效率,值得我们好好利用,下面的思维导图展示了 Stream 流提供的四大类方法:
主要有常用方法、分析统计、类型转换和其他方法这几种类型,下面就跟着我的介绍来看看吧。
3.1、常用方法
- 数据筛选
- 条件匹配
- 列表排序
- 代码调试
- 范围限制
- 跳过元素
3.1.1、数据筛选
如果想对数据列表的元素进行过滤的话,那么 filter()
方法就非常适合了,它接受一个 predicate
函数式接口用于描述筛选条件,不满足其条件的元素都会被过滤掉。此外,predicate 可适用于非常复杂的判断逻辑,代码示例如下:
List<Long> longList = Stream
// 根据给定的元素创建流
.of(3L, 98L, 46L, 16L, 9L, 9L, 9L)
// 过滤掉等于 9 的数据
.filter(i -> {
if (!i.equals(9L)) {
return true;
} else {
return false;
}})
// 转为集合并返回
.collect(Collectors.toList());
如果我们筛选的条件非常复杂的话,可以利用 predicate
函数式接口的其他方法进行编写,这种方式有点类似于 mybatisplus 中的 LambdaQueryWrapper 组成的复杂 SQL 查询方法链 ,完整的代码示例如下:
/**
* 探究流的方法
*
* @author wl
* @date 2022/10/15 21:52
*/
public class ResearchStream {
private static final List<TraverseCollection08.Person> PERSON_LIST;
static {
TraverseCollection08.Person[] people = {
new TraverseCollection08.Person("张三", 26, '男', 3856.24, "北京市海淀区"),
new TraverseCollection08.Person("李四", 79, '男', 89500.24, "湖南市长沙市下沙区"),
new TraverseCollection08.Person("王五", 31, '男', 9048.79, "南京市西湖区"),
new TraverseCollection08.Person("胡汉三", 96, '男', 121915.79, "天津市上城区"),
new TraverseCollection08.Person("刘巧梅", 23, '女', 4386.00, "杭州市西湖区"),
new TraverseCollection08.Person("吉朵阿嘎", 28, '女', 5863.77, "西藏"),
new TraverseCollection08.Person("阿的什体", 29, '男', 6874.54, "广东壮族自治区"),
new TraverseCollection08.Person("郭二车龙布", 24, '男', 6874.54, "广东壮族自治区"),
new TraverseCollection08.Person("工程抱不动", 34, '男', 6874.54, "广东壮族自治区"),
new TraverseCollection08.Person("韩美丽", 28, '女', 8495.52, "广东省深圳市"),
new TraverseCollection08.Person("王梓涵", 19, '女', -2500.00, "未知"),
};
PERSON_LIST = new ArrayList<>(Arrays.asList(people));
}
public static void main(String[] args) {
// 过滤出列表中不大于30岁的男性或地址小于等于2个字的数据
Predicate<TraverseCollection08.Person> predicate = (i -> i.getSex().equals('男'));
List<TraverseCollection08.Person> people = PERSON_LIST
// 集合转化为流
.stream()
// 根据指定的条件过滤数据,这种方式有点像 mybatisplus 中的LambdaQueryWrapper 组成的SQL查询方法链
.filter(predicate
// 大于30岁的男性
.and((i -> i.getAge() > 30))
// 或地址小于等于两个字
.or(i -> i.getAddress().length() <= 2)
// 判断是否不满足上面所有的条件,如果不满足就返回数据
.negate())
// 将流转为列表
.collect(Collectors.toList());
System.out.println("过滤出列表中大于30岁的男性或地址小于等于2个字的数据:");
// 遍历集合并输出元素
for (TraverseCollection08.Person person : people) {
System.out.println(person);
}
}
static class Person implements Serializable {
/** 姓名 */
private String name;
/** 年龄 */
private Integer age;
/** 性别 */
private Character sex;
/** 存款 */
private Double saving;
/** 现居地址 */
private String address;
/** 无参构造器方法 */
public Person() {
}
/** 全参构造器方法 */
public Person(String name, Integer age, Character sex, Double saving, String address) {
this.name = name;
this.age = age;
this.sex = sex;
this.saving = saving;
this.address = address;
}
// getter、setter 方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Character getSex() {
return sex;
}
public void setSex(Character sex) {
this.sex = sex;
}
public Double getSaving() {
return saving;
}
public void setSaving(Double saving) {
this.saving = saving;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return new StringJoiner(", ", TraverseCollection08.Person.class.getSimpleName() + "[", "]")
.add("name='" + name + "'")
.add("age=" + age)
.add("sex=" + sex)
.add("saving=" + saving)
.add("address='" + address + "'")
.toString();
}
}
}
3.1.2、条件匹配
Stream 流共有三个方法可以用于条件配置,三种类型,总有你想要的。
匹配任意一个
当我们想判断某个元素是否存在于数据中可以使用anyMatch()
方法,此方法接受一个 predicate 函数式接口用于描述用户指定的匹配条件,如果找到任意一个满足条件的数据就会返回 true,反之返回false,注意,如果流为空流,那么也会返回 false。示例代码如下:int [] arr = {16, 41, 89, 33, 40, 54, 48, 37, 35, 44, 26, 53, 28, 24, 36, 18, 61, 29, 66, 66, }; boolean flag = Arrays // 将数组转化为流 .stream(arr) // 将 Stream 流转化为 LongStream 流 .asLongStream() // 配置给定的条件,查找是否有符合条件的元素 .anyMatch(i -> i == 16L);
全部匹配
如果我们想查看流的元素是否都满足指定的条件或者判断两个列表是否相等的话,可以使用allMatch()
方法,此方法接受一个 predicate 函数式接口参数用于描述用户指定的匹配条件,所有的数据都满足给定的条件才会返回 true,反之返回 false。注意,如果流为空流,那么也会直接返回false。示例代码如下:int [] arr = {16, 41, 89, 33, 40, 54, 48, 37, 35, 44, 26, 53, 28, 24, 36, 18, 61, 29, 66, 66, }; boolean flag = Arrays // 将数组转化为流实例 .stream(arr) // 将 Strem 流对象转为 LongStream 流对象 .asLongStream() // 查看是否都匹配给定的条件 .allMatch(i -> i > 16L);
如果仅是想判断两个集合是否完全相等的话,不一定要使用流的这个方法,集合类有一个
containsAll()
方法就很好用,传入要判断的集合就可以:List<Integer> list = new ArrayList<>(0); boolean contains1 = list.contains(new ArrayList<Integer>(0)); // 如果两个集合都为空流的话,会返回false System.out.println("contains1 = " + contains1); // 不过这方法也有坑,如果传的参数是空集合,会返回true; Integer[] arr1 = {1, 2, 3, 8}; Integer[] arr2 = {1, 2, 3, 4}; boolean contains2 = new ArrayList<>(Arrays.asList(arr1)) .containsAll(new ArrayList<Integer>(0)); System.out.println("contains2 = " + contains2);
全部不匹配
这算是上一个方法的否定版本,如果元素都不匹配则会返回true,反之返回false。 noneMatch() 方法接受一个 predicate 函数式接口参数用于描述用户指定的匹配条件。注意,如果流为空流,那么会直接返回false,示例代码如下:int [] arr = {16, 41, 89, 33, 40, 54, 48, 37, 35, 44, 26, 53, 28, 24, 36, 18, 61, 29, 66, 66, }; boolean flag = Arrays // 将数组转为流实例对象 .stream(arr) // 将 Stream 流对象转为 LongStream 流对象 .asLongStream() // 查看是否全部不匹配指定的条件 .noneMatch(i -> i == 16L);
如果你的判断条件非常复杂可以搭配使用 predicate 函数式接口的其他方法,组合出需要的匹配条件,代码示例可以参考 3.1.1、数据筛选 中的 predicate 函数式接口的用法,这里不再赘述。
3.1.3、列表排序
如果我们想对列表或者流数据进行排序的话,Stream 流也有对应的方法供我们选择,但是这里需要细分两种情况:
业务类没有实现 Comparable 接口
业务类没有实现 Comparable 接口的话,可以使用sorted(comparator)
方法,此方法需要提供一个 comparator 参数用于排序此流元素,是一个有状态的中间操作,示例代码如下:// 创建测试数据 Person[] arr = new Person[10]; Random random = new Random(); for (int i = 0; i < arr.length; i++) { arr[i] = new Person("小弟" + (i + 1), random.nextInt(20) + 1, i % 2 == 0); } // 将数组转为流对象 Arrays.stream(arr) // 查看排序前的数据状态 .peek(i -> System.out.println("排序前:" + i.getAge())) // 指定排序的规则,对数据进行排序 .sorted(Comparator.comparingInt(Person::getAge)) // 查看排序后的数据状态 .peek(i -> System.out.println("排序后:" + i.getAge())) // 将流转为列表 .collect(Collectors.toList());
业务类实现了 Comparable 接口
业务类实现了 Comparable 接口或者是基本数据类型及其包装类的话,先转换为 IntStream、DoubleStream 或 LongStream 的流对象然后就可以使用无参的sorted()
方法,如果元素是对象类型的话,还是老老实实使用上面单参构造的方法最好。此外,要注意的是,如果业务类没有实现 Comparable 接口而又调用了这个方法,那么当结束操作执行时可能抛出ClassCastException
异常,示例代码如下:// 创建int类型数组 int [] arr = {16, 41, 89, 33, 40, 54, 48, 37, 35, 44, 26, 53, 28, 24, 36, 18, 61, 29, 66, 66, }; List<Integer> collect = Arrays.stream(arr) // 将数组转为流 // 查看排序前的数据状态 .peek(i -> System.out.println("排序前:" + i)) // 对数据进行排序,内部会调用其 Comparable 的实现对元素进行比较 .sorted() // 查看排序后的数据状态 .peek(i -> System.out.println("排序后:" + i)) // 将流转为 ArrayList 列表并返回 .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
冷知识
上面两个排序的方法,对于有序的流,排序是稳定的。对于无序的流,则不能保证其稳定性
3.1.4、代码调试
如果我们想调试 Stream 流,那么使用 peek(action)
方法将是个不错的选择,此方法接收一个 action 函数描述遍历时执行的动作,是一个非干涉的中间操作。它会在结果流消耗元素时对每个元素执行提供的 action 函数,然后返回由该流元素组成的流,代码示例如下:
Stream
// 使用指定的数据创建流实例对象
.of("one", "two", "three", "four")
// 筛选字符串长度大于 3 的数据
.filter(e -> e.length() > 3)
// 查看过滤数据之后的数据状态
.peek(e -> System.out.println("Filtered value: " + e))
// 将字符串中的字母都转为大写
.map(String::toUpperCase)
// 查看转为大写之后的数据状态
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
如果在使用此方法遍历数据的时候修改了源数据,那么应该提供必要的同步操作。
3.1.5、范围限制
如果我们想限制返回数据的条数,或者想从数据的左边开始截取想要的数据,那么 limit()
方法是必不可少的。此方法返回由该流元素组成的流,截取不超过指定大小的长度。此外, limit() 方法一般不会单独使用,会和其他方法配合使用,比如,iterate() 或 generate() 方法,代码示例如下:
Stream<Integer> limit = Stream
// 使用指定的数据创建流实例对象
.of(1, 2, 3, 4, 5)
// 限制返回两条数据
.limit(2);
// 遍历数据
limit.forEach(i -> System.out.println("i = " + i ));
虽然 limit() 方法通常是一个廉价的顺序操作,但是对于有序并行管道成本过高,特别的是对于较大的截取长度。 因为 limit()
方法不但可能返回插入顺序中的前 n 个元素,也可能返回任意 n 个元素。如果您的业务逻辑允许的话,使用无序的流元素(类似 generate(Supplier) 方法)或使用 unordered() 方法移除有序限制,可能会显著加速并行流的 limit()
方法的执行速度。
假如有一致性的遍历顺序需求,并且并行流中使用的 limit()
方法时遇到了遍历性能或内存利用率不佳的情况,那么使用 sequential()
方法将流切换到顺序执行也许会提高性能。
3.1.6、跳过元素
如果想要跳过数据中的前 n 条或者想从数据的右边开始截取元素,那么可以使用 skip()
方法
此方法返回一个流,其中包含此流前面开始丢弃的 n 个元素之后的剩余元素,如果此流包含的元素少于 n 个,那么可能会返回一个空流。示例代码如下:
Stream<Integer> skip = Stream.iterate(0, n -> n + 1).limit(100).skip(10);
注意,假如 n 是负数的话,此方法会抛出 IllegalArgumentException
异常。
虽然 skip(n)
方法通常是一个廉价的顺序操作,但是对于有序并行管道成本过高,特别的是对于较大的 n 值。 因为 skip(n)
方法约束不但可以跳过任意 n 个元素,而且也会跳过插入顺序中的前 n 个元素。如果您的业务逻辑允许的话,使用无序的流来源(类似 generate(Supplier)
方法)或使用 unordered()
方法移除有序限制,可能会显著加速并行流的 skip()
方法的执行。如果有一致性的遍历顺序需求,并且并行流中使用的 skip()
方法时遇到了遍历性能或内存利用率不佳的情况,使用 sequential()
方法将流切换到顺序执行也许会提高性能
3.2、分析统计
- 最大最小值
- 平均值获取
- 数据总条数
- 计算数据总和
- 汇总统计
3.2.1、最大最小值
max()
或 min()
方法会返回一个 OptionalXX 类,此类中包含最大值或最小值,如果此流为空,则返回一个空的 OptionalXX 类,示例代码如下:
// 创建测试数据
Random random = new Random();
int[] arr = new int[10];
for (int i = 0; i < 10; i++) {
arr[i] = random.nextInt(1000) + 1;
}
OptionalInt optionalIntMax = Arrays
// 将数组转为流实例对象
.stream(arr)
// 获取数据中的最大值
.max();
int max = optionalIntMax.getAsInt();
OptionalInt optionalIntMin = Arrays
// 将数组转为流实例对象
.stream(arr)
// 获取数据中的最大值
.min();
int min = optionalIntMin.getAsInt();
注意:如果使用的是 Stream 的流实例对象,还需要提供一个比较器用于比较此流的元素,示例代码如下:
// 创建测试数据
Person[] arr = new Person[10];
Random random = new Random();
for (int i = 0; i < arr.length; i++) {
arr[i] = new Person("小弟" + (i + 1), random.nextInt(20) + 1, i % 2 == 0);
}
Integer[] ages = Arrays
// 将数组转为流实例对象
.stream(arr)
// 将元素映射为类中的 age 属性
.map(Person::getAge)
// 将流实例对象转为数组
.toArray(Integer[]::new);
// 打印此数组
System.out.println("Arrays.toString(ages) = " + Arrays.toString(ages));
// 求最大值
Optional<Person> optionalMax = Arrays
// 将数组转为流实例对象
.stream(arr)
// 提供比较器比较元素大小并返回最大值
.max(Comparator.comparingInt(Person::getAge));
// 获取最大值
Person person = optionalMax.get();
// 求最小值
Optional<Person> optionalMin = Arrays
// 将数组转为流实例对象
.stream(arr)
// 提供比较器比较元素大小并返回最小值
.min((a, b) -> {
Integer aAge = a.getAge();
Integer bAge = b.getAge();
if(aAge < bAge) {
return -1;
} else {
if(aAge == bAge) {
return 0;
} else {
return 1;
}
}
});
// 获取数据的最小值并打印输出
Person person2 = optionalMin.get();
System.out.println("person = " + person);
System.out.println("person2 = " + person2);
Person 类
/**
* 人员类
*
* @author wl
* @date 2022/9/19 15:46
*/
class Person {
/** 姓名 */
private String name;
/** 年龄 */
private Integer age;
/** 性别 */
private Boolean sex;
/** 省略构造器、getter和setter方法 */
}
3.2.2、获取平均值
前言
此方法仅适用于 特定类型 的流实例,是其特有的方法。
使用 average()
方法可以获取数据的算术平均数,此方法会返回一个 OptionalDouble 类描述此流的算术平均数,即平均值。 如果此流为空,调用此方法会返回一个空的 OptionalDouble 类,示例代码如下:
int [] arr = {16, 41, 89, 33, 40, 54, 48, 37, 3 };
OptionalDouble average = Arrays
// 将数组转为流实例对象
.stream(arr)
// 获取数据的算术平均数
.average();
// 如果其平均数存在的话,获取它并打印
if(average.isPresent()) {
double ave = average.getAsDouble();
System.out.println("ave = " + ave);
}
3.2.3、获取数据总条数
如果我们想知道流的元素数量有多少,那么可以使用 count() 方法,此方法会返回此流元素的数量,如果空流则会返回0,示例代码如下:
long count = Stream
// 从 2 开始,生成以 n + 1 为规律的序列
.iterate(2, n -> n + 1)
// 限制返回前 100 条数据
.limit(100)
// 统计数据条数
.count();
你知道吗,count() 方法的底层其实非常简单,它会先调用 Stream 类的 mapToLong()
方法将每个源数据转为值为 1L 的 LongStream 对象,然后直接使用其 sum()
方法计算数据条数:
@Override
public final long count() {
return mapToLong(e -> 1L).sum();
}
3.2.4、计算数据总和
前言
此方法仅适用于 特定类型 的流实例,是其特有的方法。
如果我们想知道数据的总和,那么可以使用 sum()
方法,它会返回此流元素之和,示例代码如下:
int sum = Stream
// 生成 0 - 100 之内的随机数
.generate(() -> (int) (Math.random() * 100))
// 限制返回前 20 条数据
.limit(20)
// 将数据转为 int 类型
.mapToInt(i -> i)
// 计算数据总和
.sum();
3.2.5、汇总统计
前言
此方法仅适用于 特定类型 的流实例,是其特有的方法。
如果我们想一次性获取最大最小值、算术平均值、数据条数和数据总和,那么 summaryStatistics()
方法再适合不过了,它会返回 XXXSummaryStatistics 类,其中包含有关此流元素的各种摘要数据的汇总统计,包括最大最小值、算术平均值、数据条数和数据总和,示例代码如下:
Random random = new Random();
Integer [] arr = {16, 41, 89, 33, 40, 54, 48, 37, 35, 44, 26, 53, 28, 24, 36, 18, 61, 29, 66, 66, };
// 获取汇总统计
DoubleSummaryStatistics statistics = Arrays
// 将数组转为流实例对象
.stream(arr)
// 将 Stream 流实例转为 Double Stream 流实例
.mapToDouble(Integer::doubleValue)
// 进行统计分析
.summaryStatistics();
// 根据分析结果获取算术平均值、数据条数、最小最大值并打印输出
double average = statistics.getAverage();
long count = statistics.getCount();
double min = statistics.getMin();
double max = statistics.getMax();
double sum = statistics.getSum();
System.out.println(statistics);
3.3、类型转换
- 流的过渡
- 流、集合和数组的转换
- 更换流的执行模式
- 合并多个流
- 交换元素类型
3.3.1、流的过渡
众所周知,流的类型除了 Stream 之外,还有 IntStream、DoubleStream 和 LongStream 这三种特定类型的流,本篇来讲一下它们相互之间是如何过渡的:
先来介绍几个方法:
mapToObj() 方法返回一个对象值的流,该流由将给定函数应用于此流的元素的结果组成,这是一个非干涉且无状态的中间操作。
boxed() 方法返回由此流元素组成的流,每个元素都是其对应流元素类型的包装类,这是一个中间操作。
flatMapToInt() 方法返回一个 IntStream 流,其中包含将此流的每个元素替换为提供的映射函数应用于每个元素而生成的映射流内容的结果,每个映射流在它的内容更换到此流之后关闭,如果映射流为
null
则使用空流替代,这是一个非干涉且无状态的中间操作。asDoubleStream() 方法将此流元素转为 double 并返回一个新的 DoubleStream 流,这是一个中间操作
asLongStream() 方法将此流元素转为 long 并返回一个新的 LongStream 流,这是一个中间操作
asIntStream() 方法将此流元素转为 int 并返回一个新的 InStream 流,这是一个中间操作
Stream
如果想将流的类型转换为纯 Stream 流的,那么这些特定类型的流实例对象可以使用上面的提到的 mapToObj()
或 boxed()
方法过渡为 Stream 流对象,代码示例如下:
int [] arr = {16, 41, 89, 33, 40, 54, 48, 37, 3 };
IntStream intStream = Arrays.stream(arr);
// 使用 boxed()方法将 IntStream 流实例对象转为 Stream 流实例对象
Stream<Integer> integerStream = intStream.boxed();
// 使用 mapToObj() 方法将 IntStream 流实例对象转为 Stream 流实例对象
Stream<Integer> integerStream2 = intStream.mapToObj(i -> i);
IntStream
假如想把流的类型转换为 IntStream 的,那么可以使用 mapToInt()
方法,它会将指定的函数应用于此流的元素时产生的结果过渡为 IntStream 流,代码示例如下:
// Stream 流转换为 IntStream 流对象
Random random = new Random();
Stream<Integer> stream = Stream
// 生成带有 1-100 之间的随机数的流实例对象
.generate(() -> random.nextInt(100) + 1)
// 限制返回前 100 条数据
.limit(100);
// 将 Stream 通过指定的函数转换为 IntStream 流对象
IntStream intStream = stream.mapToInt(i -> i);
// DoubleStream 流转换为 IntStream 流对象
DoubleStream doubleStream = DoubleStream
// 带有生成指定规则的数据的流实例对象
.iterate(1.0, n -> (n * 2.0) + 1.0)
// 限制返回前 50 条数据
.limit(50);
IntStream intStream2 = doubleStream
// 将 DoubleStream 流实例转为 IntStream 流实例
.mapToInt(i -> Double.valueOf(i).intValue());
// LongStream 流转换为 IntStream 流对象
LongStream longStream = LongStream
// 生成指定规则的数据的流实例对象
.iterate(1L, n -> 3L * (n + 1L))
// 限制返回前 50 条数据
.limit(50);
IntStream intStream3 = longStream
// 将元素映射为int类型并返回,将流的类型过渡为 IntStream
.mapToInt(i -> Long.valueOf(i).intValue());
此外,flatMapToInt()
方法也可用于过渡,它的作用是将流中的每个元素通过指定的映射函数映射为单独的流,然后合并这些流为 IntStream 流并返回。代码示例如下:
// 创建测试数据
GirlFriend girl1 = new GirlFriend("cxk", 28, "唱", "跳", "rap", "打篮球");
GirlFriend girl2 = new GirlFriend("lyf", 29, "和美女谈人生");
GirlFriend girl3 = new GirlFriend("wjk", 29, "有华子不,", "抽别的我咳嗽");
// Stream 转为 IntStream
Stream<GirlFriend> stream = Stream.of(girl1, girl2, girl3);
IntStream intStream = stream
// 将多个 IntStream 流实例对象合并为一个 IntStream 流实例对象
.flatMapToInt(i -> IntStream.of(i.getAge()));
注意,flatMapToInt()
方法只适用于 Stream 流对象,至于DoubleStream 和 LongStream 流可以使用 asIntStream()
方法直接过渡。
DoubleStream
如果我们想将流的类型转为 DoubleStream 的,那么可以使用 mapToDouble()
方法, 它返回由指定函数应用于此流函数的结果组成的 DoubleStream 流,代码示例如下:
// Stream 流转换为 DoubleStream
Stream<Double> stream = Stream
// 根据指定的规则生成数据的流实例对象
.of(3.4, 7.2, 8.6, 4.3)
// 限制返回前 10 条数据
.limit(10);
DoubleStream doubleStream = stream
// 将流的类型通过指定的函数转换为 DoubleStream
.mapToDouble(i -> i);
// IntStream 流转换为 DoubleStream
IntStream intStream = IntStream
// 根据指定的规则生成数据的流实例对象
.generate(() -> (int) (Math.random() * 100))
// 限制返回前 10 条数据
.limit(10);
DoubleStream doubleStream2 = generate
// 将流的类型通过指定的函数转换为 DoubleStream
.mapToDouble(i -> i);
// LongStream 转换为 DoubleStream
LongStream longStream = LongStream
// 根据指定的规则生成数据的流实例对象
.iterate(1, n -> n + 1)
// 限制返回前 100 条数据
.limit(100);
DoubleStream doubleStream3 = iterate
// 将流的类型通过指定的函数转换为 DoubleStream
.mapToDouble(i -> i);
此外,LongStream 和 IntStream 流额外还可使用 asDoubleStream()
方法直接将流的类型转为DoubleStream 类型的流实例对象
LongStream
假如我们想要过渡流的类型为 LongStream,可以使用 mapToLong()
方法,它返回由指定函数应用于此流函数的结果组成的 LongStream 流,代码示例如下:
// Stream 流转换为 LongStream
Stream<Integer> stream = Stream
// 根据指定的规则生成数据的流实例对象
.iterate(1, n -> n + 1)
// 限制返回前 34 条数据
.limit(34);
LongStream longStream = stream
// 使用方法引用的形式将流的类型转为 LongStream
.mapToLong(Long::valueOf);
// IntStream 流转换为 LongStream
IntStream iterate = IntStream
// 根据指定的规则生成数据的流实例对象
.iterate(1, n -> n + 1)
// 限制返回前 28 条数据
.limit(28);
LongStream longStream2 = iterate
// 使用方法引用的形式将流的类型转为 LongStream
.mapToLong(Long::valueOf);
// DoubleStream 转换为 LongStream
DoubleStream doubleStream = DoubleStream
// 根据指定的规则生成数据的流实例对象
.iterate(1, n -> n + 1)
// 限制返回前 28 条数据
.limit(28);
LongStream longStream3 = doubleStream
// 使用 lambda 的形式将流的类型转为 LongStream
.mapToLong(i -> (long)(i));
此外,DoubleStream 和 IntStream 流额外还可使用 asDoubleStream() 直接将流过渡为 LongStream 流对象
3.3.2、流、集合和数组的转换
下面将介绍从流到集合或数组的转换,有好几个方法,非常常用,值得大家学习。此外,2.2、其他方式已经介绍了从数组或集合转为 Stream 流的方法,这里不再赘述。
集合
如果我们想把数据转为集合的话,可以使用下面的一些方法:
collect(Collector<? super T, A, R> collector)
方法
此方法使用 collector
参数对此流的元素执行归纳操作,此 collector
函数封装了用于 collect(Supplier, BiConsumer, BigConsumer)
方法的参数,允许集合的重复使用策略并且组成收集操作,例如,多等级的分组或分区,所以它可以转为 List、Map、Set 对象,并且可以对对象进行分组和对字符串元素进行连接。此外,这个方法是一个结束操作且仅适用于 Stream 流对象,代码示例如下:
// Stream 流转为 List 对象
List<Integer> toList = Stream
// 根据指定规则生成数据的流实例对象
.iterate(1, n -> n + 1)
// 返回前 20 条数据
.limit(20)
// 将流转为列表
.collect(Collectors.toList());
// Stream 流转为 Map 对象
Map<Integer, String> toMap = Stream
// 根据给定的数据生成流实例对象
.of(new Person("老王", 27, true)
, new Person("小李", 18, true)
, new Person("小陈", 21, true))
// 使用对象中的属性映射为 Map 对象
.collect(Collectors.toMap(Person::getAge, Person::getName));
// 也可以将其映射为其他类型的 Map 对象
ConcurrentMap<Integer, String> toMap2 = Stream
// 根据给定的数据生成流实例对象
.of(new Person("老王", 27, true)
, new Person("小李", 18, true)
, new Person("小陈", 21, true))
// 使用对象中的属性映射为 ConcurrentMap 对象
.collect(Collectors.toConcurrentMap(Person::getAge, Person::getName));
// Stream 流转为Set对象
Set<Person> collect = Stream
// 根据给定的数据生成流实例对象
.of(new Person("老王", 27, true)
, new Person("小李", 18, true)
, new Person("小陈", 21, true))
// 使用对象中的属性映射为 Set 对象
.collect(Collectors.toSet());
// Stream 根据某个属性分组转为map对象
Map<Integer, List<Person>> toMapList = Stream
// 根据给定的数据生成流实例对象
.of(new Person("老王", 27, true)
, new Person("小李", 18, true)
, new Person("小陈", 21, true))
// 使用对象中的属性对数据进行分组
.collect(Collectors.groupingBy(Person::getAge));
// String 分割字符串
String toString = Stream
// 生成0-100范围的随机数并转为字符串形式并返回作为流实例对象
.generate(() -> {
return Integer.valueOf((int) (Math.random() * 100)).toString();
})
// 使用指定的分割符、前缀和后缀连接这些数据
.collect(Collectors.joining(",", "[", "]"));
collect(Supplier<R> supplier, ObjIntConsumer<R> accumulator,BiConsumer<R, R> combiner)
方法
此方法对流的元素执行归纳操作,归纳是指一种可变结果容器归纳的值。 例如 ArrayList,元素是通过更新结果状态而不是更换其结果来合并的,适用于 IntStream、LongStream 或 DoubleStream 流对象,代码示例如下:// 将流转为 ArrayList 列表集合 ArrayList<Integer> toList = IntStream // 生成 1 到 20-1 范围的数据的流并返回 .rangeClosed(1, 20) // 将流转为 ArrayList 列表 .collect(ArrayList::new, ArrayList::add, ArrayList::addAll); // 将流实例对象转为 LinkedList 列表集合 LinkedList<Double> toList2 = DoubleStream // 生成指定规律的源数据的流实例对象 .iterate(2.0, n -> n * 10.0) // 限制返回前20条数据 .limit(20) // 将流实例对象转为 LinkedList 并返回 .collect(LinkedList::new, LinkedList::add, LinkedList::addAll); // 将流实例转为 HashSet HashSet<Long> toSet = LongStream // 通过指定的数据创建流实例对象 .of(1L, 2L, 3L) // 将流实例对象转为 HashSet 并返回 .collect(HashSet::new, HashSet::add, HashSet::addAll);
数组
如果想把数据转为数组,那么可以使用下面的方法:
toArray()
方法
这个方法可以将流转为数组,但是有一点要注意的是,其数组的类型可能会是 Object 的,只有特定类型的流实例对象才能转为对应类型的数组,示例代码如下:
// 特定类型流会返回其对应的基本数据类型数组
IntStream intStream = IntStream
// 生成指定范围的数据流实例对象
.rangeClosed(1, 10);
int[] ints = intStream
// 转为特定类型基本数据类型数组
.toArray();
// 如果是 Stream 流对象会返回一个 Object 数组
Object[] objects = Stream
// 生成指定数据的流实例对象
.of(1, 2, 3, 4)
// 转为数组并返回
.toArray();
toArray(IntFunction<A[]> generator)
方法
如果要把流转为数组,此方法会比较常用,它接受一个生成器函数返回一个包含此流元素的数组,使用提供的 generator
函数分配返回的数组,以及分区执行或调整大小可能需要的任何其他数组,这是一个结束操作。此外,此方法只适用于 Stream 流对象,其他特定类型的实例对象没有此方法也并不需要,代码示例如下:
Integer[] toArray = Stream
// 生成指定规律的数据的流实例对象
.iterate(1, i -> i + 2)
// 限制返回前十条数据
.limit(10)
// 转为 Integer 类型的数组
.toArray(Integer[]::new);
3.3.3、更换流的执行模式
Stream 流有并行和顺序两种执行顺序可以选择,顺序模式通常用于单线程,而并行模式通常用于多线程,我们通过以下的方法进行更换:
sequential()
方法
这个方法会返回一个等价的顺序模式的流实例对象,如果已经流已经是顺序模式或流的基础状态已经改为并行的话,会返回它自己。代码示例如下:Stream<Object> sequential = empty // 将流转为顺序模式的流实例对象 .sequential(); // 查看流的执行模式是否为顺序 System.out.println("流的模式:" + sequential.isParallel());
parallel()
方法
返回一个等价的并行模式的流实例对象,如果已经流已经是顺序模式或流的基础状态已经改为并行的话,会返回它自己,代码示例如下:Stream<Object> parallel = empty // 将流转为并行模式的流实例对象 .parallel(); // 查看流的执行模式是否为并行 System.out.println("流的模式:" + sequential.isParallel());
3.3.4、合并多个流
如果我们想将流的每个元素映射为流并进行合并的话,通常会使用到这个方法:
flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
方法
此方法返回一个流,其中包含将此流的每个元素替换为提供的映射函数应用于每个元素而生成的映射流内容的结果,每个映射流在它的内容更换到此流之后关闭,如果映射流为 null
则使用空流替代,这是一个非干涉且无状态的中间操作,代码示例如下:
orders
// 获取订单流中的每个订单项的属性转为流实例对象,并合并它们
.flatMap(order -> order.getLineItems().stream())
3.3.5、交换元素类型
在使用流的过程,通常我们会需要频繁或偶尔的更换流中元素的类型,一般都会用到下面这个方法:
map(Function<? super T, ? extends R> mapper)
方法(常用)
此方法返回一个流,其元素由应用于此流元素的给定函数的结果组成,是一个非干涉且无状态的中间操作,代码示例如下:
Stream<Integer> integerStream = Stream
// 生成指定数据的流实例对象
.of(1, 2, 3, 4);
Stream<Long> longStream = integerStream
// 将流元素的类型映射为 Map 并返回
.map(i -> Long.parseLong(i.toString()));
注意:虽然所有的 Stream 流类都有这个方法,但是只有 Stream 流的可以更换其元素类型,特定类型的流实例对象只能更换元素本身的值,不能更换其数据类型。
3.4、其他方法
- 获取元素
- 强大的 reduce() 函数
- 列表去重
- 流的遍历
3.4.1、获取流的元素
如果我们想直接获取流的元素,那么有两个方法可以供我们选择:
findFirst()
方法这个方法会返回一个 Optional 类描述此流的第一个元素,如果流是空的则返回一个空的 Optional 类,如果流无序(比如Map),那么可能返回任何元素。示例代码如下:
Optional<Integer> any = Stream // 生成指定数据的流实例对象 .of(1, 2, 3, 4, 5) // 找到第一个元素 .findFirst(); // 获取第一个元素 Integer integer = any.get();
此外,如果返回的元素是 null,那么可能会抛出一个 NullPointerException
异常
findAny()
方法
这个方法会返回 Optional 类描述此流的一些元素,如果流是空的则返回一个空的 Optional 类,如果找到的元素是 null,那么可能会抛出一个NullPointerException
异常,示例代码如下:Optional<Integer> any = Stream // 生成指定规律的数据的流实例对象 .iterate(2, n -> n + 1) // 找到任意一个 .findAny(); // 获取找到的元素 Integer integer = any.get();
关于这个方法,API文档中还有一段说明:
The behavior of this operation is explicitly nondeterministic; it is free to select any element in the stream. This is to allow for maximal performance in parallel operations; the cost is that multiple invocations on the same source may not return the same result. (If a stable result is desired, use findFirst() instead.)
简单翻译一下就是:
此操作的行为暗含着不确定性 —— 在流中自由选择任何元素,这是为了在并行操作中实现最佳性能,同一个源数据上的多次调用的代价是,可能会返回不同的结果。(如果需要稳定的结果,那不妨使用 findFirst() 方法替代)。
但是实际使用下来,我发现多次调用 findAny()
方法的话,只会返回第一个元素,不符合上面的说明,不知道是为什么。
3.4.2、强大的 reduce() 函数
前言
reduce()
方法是 Stream 中最强大的函数,因为求最大值、最小值、获取平均数、计算总数等等方法都是 reduce()
方法的一种特殊情况罢了,下面的代码示例展示了其用途:
// 计算 1+2+3+..+100 的值
int value = IntStream
// 使用 1 - 100 的数据创建流对象
.rangeClosed(1, 100)
// 计算数据总和
.reduce(Integer::sum)
// 返回获取的数据总和
.getAsInt();
Integer stream = Stream
// 生成指定规律的数据
.iterate(0, n -> n + 1)
// 截取前 101 条数据
.limit(101)
// 计算其总和
.reduce(0, Integer::sum);
// 计算 3! 阶乘
Integer reduce = Stream
// 使用指定的数据创建流实例对象
.of(1, 2, 3)
// 计算 3 的阶乘并返回
.reduce(1, (a, b) -> a * b);
// 把列表拼成一个整数
Integer reduce = Stream
// 使用指定的数据创建流实例对象
.of(4, 3, 5, 7, 2)
// 将所有的数据汇总并拼成一个整数
.reduce(0, (x, y) -> x * 10 + y);
// 计算列表的最大值、最小值
Integer max = Stream
// 使用指定的数据创建流实例对象
.of(1,2,3,7,5)
// 计算数据中的最大值
.reduce(Integer::max)
// 获取最大值的结果并返回
.get();
Integer min = Stream
// 使用指定的数据创建流实例对象
.of(1, 2, 3, 7, -5)
// 计算数据中的最小值
.reduce(Integer::min)
// 获取最小值的结果并返回
.get();
// 计算数据个数
Stream<Integer> integerStream = Stream
// 使用指定的数据创建流实例对象
.of(1, 2, 3);
long count = integerStream
// 将 Stream 流转为 LongStream 流
.mapToLong(i -> 1L)
// 计算数据个数
.reduce(0, Long::sum);
方法介绍
reduce(T identity, BinaryOperator<T> accumulator)
方法此方法使用提供的
identity
值以及accumulator
关联聚合函数,对此流的元素进行归纳,这个方法等同于:T result = identity; for (T element : this stream) result = accumulator.apply(result, element); return result;
且不限于顺序执行。
identity
值必须标识accumulator
函数,这意味着对于所有 T,accumulator.apply(identity, t)
等同于 T,
虽然与简单地在循环中改变循环次数相比,这似乎是一种更迂回的方式来执行聚合。但是归纳操作能更优雅地并行,不需要额外的同步并且大大降低了数据竞争的风险。此外,累加器函数必须是关联函数,代码示例如下:// 从 0 开始计算数据总和并返回 Integer sum = integers.reduce(0, (a, b) -> a+b);
reduce(BinaryOperator<T> accumulator)
方法此方法使用提供的 accumulator 关联聚合函数,对此流的元素进行归纳,并且如果有的话,返回 Optional 类用于描述归纳值,这个操作等同于:
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();
且不限于顺序执行。
累加器函数必须为标识函数,虽然与简单地在循环中改变循环次数相比,这似乎是一种更迂回的方式来执行聚合。但是归纳操作能更优雅地并行,不需要额外的同步并且大大降低了数据竞争的风险。此外,这个方法是一个归纳、非干涉且无状态的终止操作,代码示例如下:
// 计算数据总和并返回 Integer sum = integers.reduce(Integer::sum).get();
reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner)
方法
此方法使用提供的identity
值以及accumulator
关联聚合函数和combiner
联合函数,对此流的元素进行归纳,这个方法等同于:U result = identity; for (T element : this stream) result = accumulator.apply(result, element); return result;
且不限于顺序执行。
identity
值必须标识联合函数,这意味着对于所有 u,accumulator.apply(identity, u)
等同于 u,此外,组合器函数必须与累加器函数兼容;对于所有的 u 和 t, 必须坚持以下原则:combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
虽然与简单地在循环中改变循环次数相比,这似乎是一种更迂回的方式来执行聚合。但是归纳操作能更优雅地并行,不需要额外的同步并且大大降低了数据竞争的风险,代码示例如下:
Integer reduce = Stream.of(1, 2, 3) // 计算数据总和并返回 .reduce(0, Integer::sum, Integer::max);
3.4.3、列表去重
如果我们去掉数据中重复存在的,那么可以使用 distinct()
方法,此方法返回一个流,其中包含此流不同的元素,他会根据 Object.equals(Object)
方法进行比较,所以业务类必须重写 equal() 和 hashcode() 才能使用
对于有序的流来说,不同的元素选择是稳定的。对于无序的流来说,则不能保证其稳定性,这是一个有状态的中间操作,示例代码如下:
Stream
// 生成包含范围在 1 - 10 的随机数的流
.generate(() -> (int) (Math.random() *10) + 1)
// 限制返回前 100 条数据
.limit(100)
// 查看截取数据之后的情况
.peek(i -> System.out.println("limit 后:" + i))
// 对元素进行去重
.distinct()
// 查看去重后的情况
.peek(i -> System.out.println("distinct 后:" + i))
// 将流转为列表并返回
.collect(Collectors.toList());
在并行流管道保持 distinct() 方法的稳定性成本相对较高(需要操作充当完整的屏障,且伴随大量的缓存虚耗),并且稳定性通常是不需要的。此外,使用无序的流(例如 generate(Supplier) 方法)或使用 unordered() 方法移除有序限制,可能会显著提高并行流的 distinct() 方法的执行效率。
3.4.5、流的遍历
Stream 流还提供了一些方法用于遍历流元素 ,好像没什么用
forEach(XXXConsumer action)
方法
此方法对每个元素执行都会 action 函数。这是一个非干涉的结束操作,没有返回值。
对于并行流管道,这个操作不能保证遵循流的插入顺序,如果需要保证的话,那么这样做会牺牲并行性的好处。对于任意指定的元素,该操作可以在库选择的任何时间和任何线程中执行。如果动作访问了共享状态,必须增加对应的同步操作,示例代码如下:// 单线程下可以保证遍历顺序的一致性 LongStream.rangeClosed(2L, 30L) .forEach(i -> System.out.println("before range: " + i)); // 多线程就有点吃瘪了 Thread thread = new Thread(() -> { LongStream longStream = LongStream.rangeClosed(2L, 30L); longStream.forEach(i -> System.out.println("1before range: " + i)); }); thread.start(); Thread thread2 = new Thread(() -> { LongStream longStream = LongStream.rangeClosed(1L, 30L); longStream.forEach(i -> System.out.println("2before range: " + i)); }); thread2.start();
forEachOrdered(Consumer<? super T> action)
方法
这个方法对流的每个元素执行都会 action 函数。这是一个非干涉的结束操作,没有返回值。
此操作一次处理一个元素,如果有序则按插入顺序遍历。对一个元素的执行动作必定发生在下一个元素执行之前,但是对于任何给定的元素,此动作也许会在库选择的任何线程中执行。
这个方法和上面的方法唯一区别是,在多线程环境下也能保证一致的遍历顺序,示例代码如下:// 创建线程池 ExecutorService threadPool = new ThreadPoolExecutor( 2, 5, 1L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(8), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); try{ // 循环创建十个线程 for (int i = 0; i < 10; i++) { final int num = i; // 创建Stream 流并在线程中执行遍历操作 LongStream longStream = LongStream.rangeClosed(1L, 5L); threadPool.execute(()-> { longStream.forEachOrdered(j -> System.out.println(num + "before range: " + j)); }); } } catch (Exception e) { e.printStackTrace(); } finally { threadPool.shutdown(); // 关闭线程池 }
冷知识
集合有一个自带的 forEach() 方法,可以用于遍历,不需要转成流再进行遍历