Java8 Stream
java8 的 Stream
开始之前先叙述一些相关的概念,
-
流的操作类型分为两种:
-
Intermediate:一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
-
Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就会被,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。
-
short-circuiting
对于一个无限大(infinite/unbounded)的 Stream,返回有限的新的Stream或者能在有限的时间内返回结果
-
通过以上的概念,可以归类如下的方法
-
Intermediate:
map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
-
Terminal:
forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
-
Short-circuiting:
anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
生成Stream的几种方式
- 从 Collection 和数组 (最常用)
// 1. 从Collection中生成
List list = new ArrayList<String>(){{add("A");add("B");}};
Stream<String> stream = list.stream();
// 2. 下面是从数组生成
int[] arr = new int[]{1,2,3};
IntStream stream = Arrays.stream(arr);
// or
Stream.of(1,2,3)
- 其他
// 从BufferedReader中生成
java.io.BufferedReader.lines()
// 通过一些静态的方法
java.util.stream.IntStream.range()
java.nio.file.Files.walk()
java.nio.file.Files.lines()
-
自定义
通过java.util.Spliterator 可以自己构建,这里就不展开了,有兴趣的可以参考下面两篇文章
Stream 概览
先构建一个对象,下面所有的方法会用到这个基础对象,以及构造一个简单的persons,供下文使用
class Person{
private String name;
private Integer age;
// 省略getter,setter方法
public Person(String name, Integer age){
this.name = name;
this.age = age;
}
}
List<Person> persons = new ArrayList<>();
persons.add(new Person("jack",17));
persons.add(new Person("rose",18));
persons.add(new Person("小明",17));
map/flatMap
map会对流中的每个元素应用map中的lambda表达式
List<String> result = persons.stream().map(
a -> a.getName().toUpperCase()).collect(Collectors.toList());
// result中的元素为 [JACK, ROSE, 小明]
上面的意思是取流中的每个元素(这里就是Person),把name属性转为大写,最后的collect(Collectors.toList())就是上面说的Terminal操作,因此我们可以获取到一个List
上面是一对一的操作,如果是一对多的,则可以使用flatMap
Stream<List<Integer>> inputStream = Stream.of(
Arrays.asList(1),
Arrays.asList(2, 3),
Arrays.asList(4, 5, 6)
);
Stream<Integer> outputStream = inputStream.flatMap((childList) -> childList.stream());
flatMap抽取出底层的元素放在一起,就是上面的例子中把三个list中的元素全部抽取出来,最后返回一个数字流
filter
List<Person> collect = persons.stream()
.filter(a -> a.getAge() > 17).collect(Collectors.toList());
// 过滤出流中所有age > 17的元素,返回一个新的流
filter接受一个Predicate(谓词)参数,返回符合为true的元素
distinct
顾名思义就是去重的操
List<Integer> integers = Arrays.asList(1, 1, 1);
List<Integer> result = integers.stream().distinct().collect(Collectors.toList());
// 最终result里面的元素只有一个[1]
需要注意的是,每个元素去重基于的 Object#equals(Object) 方法,在一些场合使用Collection中的removeIf()默认方法更加合适
foreach
Stream提供了 foreach 方法来遍历流中的每个数据
persons.stream().forEach(a -> System.out.println(a.getName()));
// 更简单的写法如下,因为List接口中有了一个forEach默认方法,关于接口默认方法这里就不阐述了
persons.forEach(a -> System.out.println(a.getName()));
sorted/max/min
sorted会对流中的元素做排序,默认是从小大到的顺序
List<Person> result = persons.stream().sorted(Comparator.comparing(Person::getAge)).collect(Collectors.toList());
如果可以Stream 进行各类 map、filter、limit、skip 甚至 distinct 来减少元素数量后,再排序,这序明显缩短程序执行时间
max/min操作类似,不过他们各自返回最大值和最小值,如果是对象都是int,long,double类型,可以使用IntStream、LongStream、DoubleStream,避免拆箱装箱的性能消耗。这个时候可以使用如下操作
OptionalLong max = persons.stream().mapToLong(Person::getAge).max();
System.out.println(max.getAsLong()); //输出 18
peek
之前我们说过流只能被terminal 操作一次,如果有类似的场景需要多次terminal 操作,peek可以达到目的
// peek方法方法会使用一个Consumer消费流中的元素,但是返回的流还是包含原来的流中的元素。
OptionalLong max = persons.stream().peek(a -> System.out.println(a.getName())).mapToLong(Person::getAge).max();
System.out.println(max.getAsLong());
match
// 流中所有的元素都满足谓词中的条件才返回true
public boolean allMatch(Predicate<? super T> predicate)
// 流中所有的元素有一个满足谓词中的条件就返回true
public boolean anyMatch(Predicate<? super T> predicate)
// 流中所有的元素都不满足谓词中的条件才返回true
public boolean noneMatch(Predicate<? super T> predicate)
reduce
reduce方法返回单个的结果值,并且reduce操作每处理一个元素总是创建一个新值。reduce主要有如下三个方法
// 最常用的,identity参数是初始值,accumulator是累加器,其方法签名是 apply(T t,U u),累加的值会被赋值给下次执行方法的第一个参数,也就是t
T reduce(T identity, BinaryOperator<T> accumulator);
// 没有初始值,返回的是一个Optional,因为null是不安全的
Optional<T> reduce(BinaryOperator<T> accumulator);
// 第三个参数是使用并行流(parallelStream)时,合并每个线程的操作
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
所以常用的方法有average, sum, min, max, count,使用reduce方法都可实现。
// 实现加法
Integer result = Stream.of(1, 2, 3).stream().reduce(0, (a, b) -> a + b); // result:6
// 实现减法
Integer result = Stream.of(1, 2, 3).reduce(10, (a, b) -> a - b); // result:10 - 6 = 4
collect(收集结果)
collect可以把流收集起来,可以是一个List,Map以及分组等
List<Integer> result = Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, List::addAll);
// result: [1,2,3]
- 第一个方法生成一个新的ArrayList;
- 第二个方法中第一个参数是前面生成的ArrayList对象,第二个参数是stream中包含的元素,方法体就是把stream中的元素加入ArrayList对象中。第二个方法被反复调用直到原stream的元素被消费完毕;
- 第三个方法也是接受两个参数,这两个都是ArrayList类型的,方法体就是把第二个ArrayList全部加入到第一个中;
代码看起来并不清真,也不容易理解。因此,还有另外的简便写法,下面会提到
Collector
Collectors实现了Collector接口,提供了很多有用的方法
toList/toSet
List<Integer> result = Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, List::addAll);
// 可以简写为
List<Integer> result = Stream.of(1, 2, 3).collect(Collectors.toList());
// 如果需要去重,可以使用toSet
Set<Integer> result = Stream.of(1, 1, 3).collect(Collectors.toSet());
toMap
// Function.identity() 的作用等同于 a -> a,即输入等于输出
Map<Integer, Person> collect = persons.stream().collect(Collectors.toMap(Person::getAge, Function.identity()));
// 这里就要有坑了,如果key相同的话,会抛出异常,可以通过指定第三个参数指定合并方式
Map<Integer, Person> collect = persons.stream().collect(Collectors.toMap(Person::getAge, Function.identity(),(a,b) -> a)); // 这里我们指定了如果有冲突,取之前的那个
以为这样就没有坑了么?不对,如果在value为null的时候,还会抛出 NPE的异常,HashMap中的merge代码中有如下这一行。
所以,最好不要使用这个方法,使用如下的写法代替
Map<Integer,Person> map = new HashMap<>();
persons.forEach(a -> map.put(a.getAge(),a));