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()

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));
posted @ 2018-04-23 23:59  hinsy  阅读(367)  评论(0编辑  收藏  举报