开始使用流
- Java 8 中的 Stream 俗称为流,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念
- Stream 用于对集合对象进行各种非常便利、高效的聚合操作,或者大批量数据操作
- Stream API 借助于 Lambda 表达式,极大的提高编程效率和程序可读性
- 同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势
- 通过下面的例子我们可以初步体会到使用 Stream 处理集合的便利性
初探Stream
- 有如下一个 List 集合,现要从中筛选出以
J
开头的元素,然后转换为大写,最后输出结果- Java 8之前我们是这样做的:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift");
List<String> filterList = new ArrayList<>();
for (String str : list) {
if (str.startsWith("J")) {
filterList.add(str.toUpperCase());
}
}
for (String str : filterList) {
System.out.println(str);
}
}
}
- 为了筛选集合我们进行了两次外部迭代,并且还创建了一个用来临时存放筛选元素的集合对象
- 借助Java 8中的Stream我们可以极大的简化这个处理过程:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift");
list.stream()
.filter(s -> s.startsWith("J"))
.map(String::toUpperCase)
.forEach(System.out::println);
}
}
是不是很方便?上面的例子中,集合使用 stream
方法创建了一个流,然后使用 filter
和 map
方法来处理这个集合,它们统称为 中间操作。中间操作都会返回另一个流,以便于将各种对集合的操作连接起来形成一条流水线。最后我们使用了 forEach
方法迭代筛选结果,这种位于流的末端,对流进行处理并且生成结果的方法称为 终端操作。
总而言之,流的使用一般包括三件事情:
- 一个 数据源(如集合)来执行一个查询
- 一个 中间操作 链,形成一条流的流水线
- 一个 终端操作,执行流水线,并能生成结果
下表列出了流中常见的中间操作和终端操作:
操作 | 类型 | 返回类型 | 使用的类型 / 函数式接口 | 函数描述符 |
---|---|---|---|---|
filter | 中间 | Stream<T> | Predicate<T> | T -> boolean |
distinct | 中间 | Stream<T> | ||
skip | 中间 | Stream<T> | long | |
limit | 中间 | Stream<T> | long | |
map | 中间 | Stream<R> | Function<T, R> | T -> R |
flatMap | 中间 | Stream<R> | Function<T, Stream<R>> | T -> Stream<R> |
sorted | 中间 | Stream<T> | Comparator<T> | (T, T) -> int |
anyMatch | 终端 | boolean | Predicate<T> | T -> boolean |
noneMatch | 终端 | boolean | Predicate<T> | T -> boolean |
allMatch | 终端 | boolean | Predicate<T> | T -> boolean |
findAny | 终端 | Optional<T> | ||
findFirst | 终端 | Optional<T> | ||
forEach | 终端 | void | Consumer<T> | T -> void |
collect | 终端 | R | Collector<T, A, R> | |
reduce | 终端 | Optional<T> | BinaryOperator<T> | (T, T) -> T |
count | 终端 | long |
- 下面详细介绍这些操作的使用
- 除了特殊说明,使用下面这个集合作为演示:
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
中间操作
filter
- Streams 接口支持
filter
方法,该方法接收一个Predicate<T>
,函数描述符为T -> boolean
,用于对集合进行筛选,返回所有满足的元素:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
list.stream()
.filter(s -> s.contains("#"))
.forEach(System.out::println);
}
}
- 结果输出
C#
distinct
distinct
方法用于排除
流中重复的元素,类似于 SQL 中的 distinct 操作- 比如筛选中集合中所有的偶数,并排除重复的结果:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i % 2 == 0)
.distinct()
.forEach(System.out::println);
}
}
- 结果输出
2 4
skip
skip(n)
方法用于跳过流中的前n个元素
,如果集合元素小于n,则返回空流- 比如筛选出以
J
开头的元素,并排除第一个:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
list.stream()
.filter(s -> s.startsWith("J"))
.skip(1)
.forEach(System.out::println);
}
}
- 结果输出
JavaScript
limit
limit(n)
方法返回一个长度不超过n
的流,比如下面的例子将输出Java JavaScript python
:- 例如你输入的 3,返回的就是3,不会超过3
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
list.stream()
.limit(3)
.forEach(System.out::println);
}
}
map
map
方法接收一个函数作为参数- 这个函数会被应用到每个元素上,并将其映射成一个新的元素
- 如:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
list.stream()
.map(String::length)
.forEach(System.out::println);
}
}
- 结果输出
4 10 6 3 2 6 5 3 4
map
还支持将流特化为指定的原始类型的流,如通过mapToInt
,mapToDouble
和mapToLong
方法,可以将流转换为IntStream
,DoubleStream
和LongStream
- 特化后的流支持
sum
,min
和max
方法来对流中的元素进行计算- 比如:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
IntStream intStream = numbers.stream().mapToInt(a -> a);
System.out.println(intStream.sum());
}
}
- 也可以通过下面的方法,将
IntStream
转换为Stream
:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
IntStream intStream = numbers.stream().mapToInt(a -> a);
Stream<Integer> s = intStream.boxed();
}
}
flatMap
flatMap
用于将多个流合并成一个流,俗称流的扁平化- 这么说有点抽象,举个例子,比如现在需要将 list 中的各个元素拆分为一个个字母,并过滤掉重复的结果,你可能会这样做:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
list.stream()
.map(s -> s.split(""))
.distinct()
.forEach(System.out::println);
}
}
- 输出结果如下:
[Ljava.lang.String;@58372a00
[Ljava.lang.String;@4dd8dc3
[Ljava.lang.String;@6d03e736
[Ljava.lang.String;@568db2f2
[Ljava.lang.String;@378bf509
[Ljava.lang.String;@5fd0d5ae
[Ljava.lang.String;@2d98a335
[Ljava.lang.String;@16b98e56
[Ljava.lang.String;@7ef20235
- 这明显不符合我们的预期
- 实际上在
map(s -> s.split(""))
操作后,返回了一个Stream<String[]>
类型的流,所以输出结果为每个数组对象的句柄,而我们真正想要的结果是Stream<String>
!- 在 Stream 中,可以使用
Arrays.stream()
方法来将数组转换为流,改造上面的方法:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
list.stream()
.map(s -> s.split(""))
.map(Arrays::stream)
.distinct()
.forEach(System.out::println);
}
}
- 输出结果如下:
java.util.stream.ReferencePipeline$Head@568db2f2
java.util.stream.ReferencePipeline$Head@378bf509
java.util.stream.ReferencePipeline$Head@5fd0d5ae
java.util.stream.ReferencePipeline$Head@2d98a335
java.util.stream.ReferencePipeline$Head@16b98e56
java.util.stream.ReferencePipeline$Head@7ef20235
java.util.stream.ReferencePipeline$Head@27d6c5e0
java.util.stream.ReferencePipeline$Head@4f3f5b24
java.util.stream.ReferencePipeline$Head@15aeb7ab
- 因为上面的流经过
map(Arrays::stream)
处理后,将每个数组变成了一个新的流,返回结果为流的数组Stream<String>[]
,所以输出是各个流的句柄- 我们还需将这些新的流连接成一个流,使用
flatMap
来改写上面的例子:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
list.stream()
.map(s -> s.split(""))
.flatMap(Arrays::stream)
.distinct()
.forEach(s -> System.out.print(s + " "));
}
}
- 输出结果如下:
J a v S c r i p t y h o n P H C # G l g w f + R u b
- 和
map
类似,flatMap
方法也有相应的原始类型特化方法,如flatMapToInt
等
终端操作
anyMatch
anyMatch
方法用于判断流中是否有符合判断条件的元素,返回值为boolean类型
- 比如判断 list 中是否含有
SQL
元素:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
System.out.println(list.stream()
.anyMatch(s -> "SQL".equals(s)));
}
}
allMatch
allMatch
方法用于判断流中是否所有元素都满足给定的判断条件,返回值为boolean类型
- 比如判断 list 中是否所有元素长度都不大于10:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
System.out.println(list.stream()
.allMatch(s -> s.length() <= 10));
}
}
注意点:java8 stream接口终端操作allMatch 当list为空集合的一些思考
noneMatch
noneMatch
方法用于判断流中是否所有元素都不满足给定的判断条件,返回值为boolean类型
- 比如判断 list 中不存在长度大于10的元素:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
System.out.println(list.stream()
.noneMatch(s -> s.length() > 10));
}
}
findAny
findAny
方法用于返回流中的任意元素的 Optional 类型- 例如筛选出 list 中任意一个以
J
开头的元素,如果存在,则输出它:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
list.stream()
.filter(s -> s.startsWith("J"))
.findAny()
.ifPresent(System.out::println);
}
}
findFirst
findFirst
方法用于返回流中的第一个元素的 Optional 类型- 例如筛选出 list 中长度大于 5 的元素,如果存在,则输出第一个:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
list.stream()
.filter(s -> s.length() > 5)
.findFirst()
.ifPresent(System.out::println);
}
}
reduce
reduce
函数从字面上来看就是压缩,缩减的意思,它可以用于数字类型的流的求和,求最大值和最小值。如对numbers中的元素求和:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
System.out.println(numbers.stream().reduce(0, Integer::sum));
}
}
reduce
函数也可以不指定初始值,但这时候将返回一个Optional
对象,比如求最大值和最小值:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.reduce(Integer::max)
.ifPresent(System.out::println);
numbers.stream()
.reduce(Integer::min)
.ifPresent(System.out::println);
}
}
forEach
forEach
用于迭代流中的每个元素,最为常见的就是迭代输出,如:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
list.stream().forEach(System.out::println);
}
}
count
count
方法用于统计流中的元素的个数,比如:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
System.out.println(list.stream().count());
}
}
collect
collect
方法用于收集流中的元素,并放到不同类型的结果中,比如List
、Set
或者Map
- 举个例子:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "JavaScript", "python", "PHP", "C#", "Golang", "Swift", "C++", "Ruby");
List<String> filterList = list.stream().filter(s -> s.startsWith("J")).collect(Collectors.toList());
System.out.println(filterList);
}
}
- 如果需要以
Set
来替代List
,只需要使用Collectors.toSet()
就好了
流的构建
- 除了使用集合对象的
stream
方法构建流之外,我们可以手动构建一些流
数值范围构建
IntStream
和LongStream
对象支持range
和rangeClosed
方法来构建数值流- 这两个方法都是第一个参数接受起始值,第二个参数接受结束值
- 但
range
是不包含结束值的,而rangeClosed
则包含结束值- 比如对 1 到 100 的整数求和:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
System.out.println(IntStream.rangeClosed(1, 100).sum());
}
}
由值构建
- 静态方法
Stream.of
可以显式值创建一个流- 它可以接受任意数量的参数
- 例如,以下代码直接使用
Stream.of
创建了一个字符串流:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
Stream<String> s = Stream.of("Java", "JavaScript", "C++", "Ruby");
}
}
- 也可以使用
Stream.empty()
构建一个空流:
Stream<Object> emptyStream = Stream.empty();
由数组构建
- 静态方法
Arrays.stream
可以通过数组创建一个流- 它接受一个数组作为参数
- 例如:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(arr);
}
}
由文件生成流
java.nio.file.Files
中的很多静态方法都会返回一个流- 例如
Files.lines
方法会返回一个由指定文件中的各行构成的字符串流- 比如统计一个文件中共有多少个字:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
long wordCout = 0L;
try (Stream<String> lines = Files.lines(Paths.get("file.txt"), Charset.defaultCharset())) {
wordCout = lines.map(l -> l.split(""))
.flatMap(Arrays::stream)
.count();
} catch (Exception ignore) {
}
System.out.println(wordCout);
}
}
由函数构造
- Stream API 提供了两个静态方法来从函数生成流:
Stream.iterate
和Stream.generate
- 这两个操作可以创建所谓的无限流
- 比如下面的例子构建了 10 个偶数:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
Stream.iterate(0, n -> n + 2)
.limit(10).forEach(System.out::println);
}
}
iterate
方法接受一个初始值(在这里是0)还有一个依次应用在每个产生的新值上的 Lambda(UnaryOperator类型)- 这里,我们使用 Lambda
n -> n + 2
,返回的是前一个元素加上 2- 因此,
iterate
方法生成了一个所有正偶数的流:流的第一个元素是初始值0- 然后加上 2 来生成新的值 2,再加上 2 来得到新的值 4,以此类推
- 与
iterate
方法类似,generate
方法也可让你按需生成一个无限流- 但
generate
不是依次对每个新生成的值应用函数,比如下面的例子生成了 5 个 0 到 1 之间的随机双精度数:
/**
* @author BNTang
**/
public class Demo {
public static void main(String[] args) {
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
}
}
- 输出结果如下:
0.4477477019693912
0.8866972547736678
0.6893219838296453
0.3768607796229386
0.9647978867306028