作者: 一字马胡
转载标志 【2017-11-03】
更新日志
日期 | 更新内容 | 备注 |
---|---|---|
2017-11-03 | 添加转载标志 | 持续更新 |
Java Stream概述
Java Stream是一系列对集合便利操作的工具集,可以对各种数据结构的数据进行聚合操作,比如对List元素的去重操作,对Set集合的分组操作等等,使用Stream API可以很方便的实现一些我们平常需要些大段代码来实现的功能。最为重要的一点是Stream使用了Fork/Join框架对任务进行拆分运行,可以非常高效的处理我们的各种操作。
Stream API是一套专注操作的接口,它不缓存任何数据,对数据进行操作之前都会创建 一个新的Stream然后再操作,也就是说不管进行了多少个Stream操作,数据源还是不会变,看下面简单的例子:
String[] nameArray = {"hujian", "libai", "hujian", "wanganshi"};
Stream.of(nameArray)
.parallel()
.distinct()
.map(String::toUpperCase)
.collect(Collectors.toCollection(ArrayList::new))
.forEach(System.out::println);
for(int i = 0;i < nameArray.length; i ++) {
System.out.println("->" + nameArray[i]);
}
//output
HUJIAN
LIBAI
WANGANSHI
->hujian
->libai
->hujian
->wanganshi
可以发现,尽管我们对源头数组施加了很多操作,但是最后数组内容都没有变化,这也就说明了每一个Stream操作都会新建一个Stream不会破坏源数据的完整性。
Stream API中的操作分为两类:
Intermediate:一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据操作,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。
还有一种操作被称为 short-circuiting。用以指:
- 对于一个 intermediate 操作,如果它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream。
- 对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。
一般的使用Stream API的流程是:
- 创建一个Stream,可以通过多种方式来创建一个Stream,下文中会专门写到
- 对Stream进行一系列的Intermediate操作,但是需要注意的是操作并不会立刻执行,而是会等待一个Terminal类型的操作之后才会执行
- 进行一个Terminal操作,使得所有施加在Stream的操作生效。
我们会对Stream API的执行有一个误区,比如上面我们的代码,首先施加了去重的操作,接着使用map进行大写转换,然后将Stream变化为一个ArrayList,最后输出List的元素内容。进行了四个操作,那么Stream API会进行四重循环来做完这套操作呢?显然是不会这样做的,Stream API记录了所有需要在Stream上执行的操作,然后在遇到terminal操作的时候,会对同一个Stream的元素进行整套操作,所以循环的应该是操作集合,而不是对元素进行循环执行。比如上面的四个操作,会对每一个元素一次执行四个操作,使人迷惑的是去重如何操作呢?Stream API使用了HashSet来操作,Set当然可以去重,我们只需要对每一个元素都add到HashSet中去,就达到去重的目的了。对于一些那一理解的操作,我们应该去阅读源码,虽然有些时候看不懂源码,因为源码过于庞大和复杂,但是可以抓住重点,寻找一些我们希望看到的信息,大概明白流程也就可以了。
如何创建Stream
使用Stream API的第一步当然是创建一个Stream,有多种方法来创建Stream。下面分为几类:
- 从Collection创建Stream
- 从数组创建Stream
- 使用Stream提供的IntStream、LongStream、DoubleStream
- 使用BufferedReader创建Stream
- 使用自己的Supplier来创建Stream
下面的代码展示了这些这些创建方法的使用方法:
import java.io.BufferedReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
/**
* Created by hujian06 on 2017/9/28.
*
*
*/
public class StreamDemo {
/*some type of creating Stream<?>*/
enum TYPE {
FROM_COLLECTION,
FROM_ARRAY_0,
FROM_ARRAY_1,
FROM_BUFFERED_READER,
INT_STREAM,
LONG_STREAM,
DOUBLE_STREAM,
FROM_FILE_WALK,
FROM_SUPPLIER
}
/**
* Creating a Stream
* @param type the type
* @param collection collection
* @param array array
* @param bufferedReader bufferedReader
* @param <T> type
* @return the Stream
*/
@SuppressWarnings(value = "unchecked")
private static <T> Stream<T> create(TYPE type, Collection<?> collection, T[] array,
BufferedReader bufferedReader, Supplier<T> supplier)
throws Exception {
Stream<T> stream = null;
switch (type) {
case FROM_COLLECTION:
if (collection == null) throw new Exception("NPE of Collection");
stream = (Stream<T>) collection.stream();
break;
case FROM_ARRAY_0:
if (array == null) throw new Exception("NPE of Array");
stream = Arrays.stream(array);
break;
case FROM_ARRAY_1:
if (array == null) throw new Exception("NPE of Array");
stream = Stream.of(array);
break;
case FROM_BUFFERED_READER:
if (bufferedReader == null) throw new Exception("NPE of bufferedReader");
stream = (Stream<T>) bufferedReader.lines();
break;
case INT_STREAM:
//stream = (Stream<T>) IntStream.range(0, 1000); //mock
break;
case LONG_STREAM:
//stream = (Stream<T>) LongStream.range(0, 100000);//mock
break;
case DOUBLE_STREAM:
//stream = (Stream<T>) DoubleStream.of(3.1415926); //mock
break;
case FROM_FILE_WALK:
break;
case FROM_SUPPLIER:
if (supplier == null) throw new Exception("NPE of supplier");
stream = Stream.generate(supplier);
break;
}
return stream;
}
//loop to generate.
private static Supplier<Integer> listSupplier = () -> {
Random random = new Random(47);
return random.nextInt();
};
public static void main(String ... args) throws Exception {
String[] nameArray = {"hujian", "libai", "hujian"};
List<String> nameList = new ArrayList<>();
Stream<String> stringStream = create(TYPE.FROM_ARRAY_0, null, nameArray, null, null);
nameList = stringStream.collect(Collectors.toCollection(ArrayList::new));
Stream<String> stringStream1 = create(TYPE.FROM_COLLECTION, nameList, null, null, null);
//stringStream.distinct().forEach(System.out::println);
stringStream1.forEach(System.out::println);
Stream<Integer> integerStream = create(TYPE.FROM_SUPPLIER, null, null, null, listSupplier);
integerStream.forEach(System.out::println);
}
}
关于更加具体和更多创建Stream的方式,可以查阅更多的资料或者直接阅读官方文档来学习。
Stream操作
上文中介绍了如何创建Stream的一些方法,本节将学习Stream提供的操作API。下面的图片展示了Stream接口的所以方法:
本文将选择一些常用的API来说明,不会涉及所有的API。
- Intermediate类型的操作
api | description | function | code |
---|---|---|---|
filter | Returns a stream consisting of the elements of this stream that match the given predicate. | 过滤器,将不需要的内容过滤掉,只留下我们想要的数据 | |
map | Returns a stream consisting of the results of applying the given function to the elements of this stream | 将数据转换为另外一种格式的操作,比如我们可以将String类型的数据转换为全是大写字母的String | |
mapToInt | Returns an {@code IntStream} consisting of the results of applying the given function to the elements of this stream. | 属于map,但是更为具体,指定了将要转换达到的类型设定为int,同类型的还有mapToLong,maptoDouble,下面不再叙述 | integerStream.mapToInt(value -> value + 100); |
flatMap | Returns a stream consisting of the results of replacing each element of this stream with the contents of a mapped stream produced by applying the provided mapping function to each element. Each mapped stream is {@link java.util.stream.BaseStream#close() closed} after its contents have been placed into this stream. (If a mapped stream is {@code null} an empty stream is used, instead.) | map操作达到的效果是一对一的,也就是只会生成一个转换后的元素,而flatMap则是一对多的,使得对每一个上游元素,你都可以生成一个新的Stream,这种处理模式将底层数据扁平化,后面的代码展示了如何使用flatMap | integerStream.flatMap((Function<Integer, Stream<?>>) integer -> Stream.of(integer)); |
flatMapToLong | * | 更加具体的指定了需要转换成的数据类型,此操作希望将上游的每一个元素都转换为一个LongStream,榆次类似的还要flatMapToInt、flatMapToDoble,下文中不再叙述 | integerStream.flatMapToLong(integer -> LongStream.of(integer)); |
distinct | * | 这是比较常用的操作,可以对元素类型去重,Stream使用HashSet来实现去重 | |
sorted | * | 排序,使用自然排序 | Stream.of(arrays).sorted().forEach(System.out::println); |
sorted(Com[arator) | * | 带参数的排序操作,参数由你指定排序的规则 | |
limit | Returns a stream consisting of the elements of this stream, truncated to be no longer than {@code maxSize} in length. | 用于限流,Stream最多只会取limit设定的元素个数到下游 | Stream.of(1,2,3,4,5).limit(2).forEach(System.out::println); |
skip | * | 和limit类似,但是skip是跳过前几个元素,然后再将接下来的元素传送到下游操作 | Stream.of(1,2,3,4,5).skip(2).forEach(System.out::println); |
- Terminal类型的操作
api | description | function | code |
---|---|---|---|
forEach | * | 用于遍历元素 | |
forEachOrder | * | forEach使用并行,而forEachOrder使用串行 | |
toArray | * | 转换为array | |
reduce | * | 组合元素,提供一个起始值,然后从这个值迭代,每个元素都将参与设定的运算,适合对元素求和、求最大最小值等操作 | int sum = Stream.of(1,2,3).reduce(0, (integer, integer2) -> integer + integer2); |
collect | * | 将Stream转换为一个Collection | Stream.of(1,2,3,4,5).collect(Collectors.toCollection(ArrayList::new)).forEach(System.out::println); |
min/max/count | * | * | * |
- short-circuiting类型的操作
api | description | function | code |
---|---|---|---|
anyMatch | 任意元素满足即可 | Stream.of(1,2,3,4,5).anyMatch(integer -> integer % 2 == 0); | |
allMatch | 所有元素都需要满足 | Stream.of(1, 2, 3).allMatch(integer -> integer > 2); | |
noneMatch | 所有元素都不满足 | Stream.of(1,2,3).noneMatch(integer -> integer % 2 == 0); | |
findFirst | 返回Stream的第一个元素或者空 | ||
findAny | 返回任何一个元素然后就结束了 | ||
limit | 上文已经说过 |
扩展
值得注意的是,Optional类也实现了一些类似Stream API的操作。下面的图片展示了Optional类提供的一些操作,在项目中,应该尽量使用Optional类来避免null,而且Optional也提供了强大的数据处理能力,结合Optional和Stream,势必会提高我们的工作效率。