已复制
全屏展示
复制代码

Java 之惰性计算 stream 总结


· 10 min read

Java从8开始,不但引入了Lambda表达式,还引入了一个全新的流式API:Stream API。它位于java.util.stream包中。这个Stream不同于java.io的InputStream和OutputStream,它代表的是任意Java对象的序列,它类似Python中的迭代器。

一. 创建Stream

1.1 Stream.of()

创建Stream最简单的方式是直接用Stream.of()静态方法,传入可变参数即创建了一个能输出确定元素的Stream,虽然这种方式基本上没啥实质性用途,但测试的时候很方便。

import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("A", "B", "C", "D");
        stream.forEach(System.out::println);
    }
}

1.2 基于数组或Collection

  • 把数组变成Stream使用Arrays.strem()方法。
  • 对于CollectionList、Set、Queue等),直接调用stream()方法就可以获得Stream
import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
        stream1.forEach(System.out::println);

        List<String> stringOne = new ArrayList<>();
        stringOne.add("a");
        stringOne.add("b");
        stringOne.add("c");
        Stream<String> stream2 = stringOne.stream();
        stream2.forEach(System.out::println);

        Set<String> setOne = new HashSet<>();
        setOne.add("1");
        setOne.add("2");
        setOne.add("3");
        Stream<String> stream3 = setOne.stream();
        stream3.forEach(System.out::println);
    }
}

1.3 基于Supplier

创建Stream还可以通过Stream.generate()方法,它需要传入一个Supplier对象,基于Supplier创建的Stream会不断调用Supplier.get()方法来不断产生下一个元素,这种Stream保存的不是元素,而是算法,它可以用来表示无限序列。例如,我们编写一个能不断生成自然数的Supplier,它的代码非常简单,每次调用get()方法,就生成下一个自然数:

import java.util.function.Supplier;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Stream<Integer> natual = Stream.generate(new NatualSupplier());
        natual.limit(20).forEach(System.out::println);
    }
}

class NatualSupplier implements Supplier<Integer> {
    int n = 0;
    public Integer get() {
        n++;
        return n;
    }
}

二. 基本类型

因为Java的范型不支持基本类型,所以我们无法用Stream<int>这样的类型,会发生编译错误。为了保存int,只能使用String<Integer>,但这样会产生频繁的装箱、拆箱操作。为了提高效率,Java标准库提供了IntStreamLongStreamDoubleStream这三种使用基本类型的Stream,它们的使用方法和范型Stream没有大的区别,设计这三个Stream的目的是提高运行效率:

IntStream is = Arrays.stream(new int[] { 1, 2, 3 });                         // 将int[]数组变为IntStream
LongStream ls = List.of("1", "2", "3").stream().mapToLong(Long::parseLong);  // 将Stream<String>转换为LongStream

三. 常用Stream函数

3.1 Stream.map()

Stream.map()Stream最常用的一个转换方法,它把一个Stream转换为另一个Stream。所谓map操作,就是把一种操作运算,映射到一个序列的每一个元素上。如果我们查看Stream的源码,会发现map()方法接收的对象是Function接口对象,它定义了一个apply()方法,负责把一个T类型转换成R类型

import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
        Stream<Integer> s2 = s.map(n -> n * n);
        s2.forEach(System.out::println);
    }
}
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Stream<String> s = Stream.of(" Apple ", " pear ", " ORANGE", " BaNaNa ");
        s.map(String::trim)
                .map(String::toLowerCase)
                .map(String::length)
                .forEach(System.out::println);
    }
}

3.2 Stream.filter()

所谓filter()操作,就是对一个Stream的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Streamfilter()方法接收的对象是Predicate接口对象,它定义了一个test()方法,负责判断元素是否符合条件。

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        IntStream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
                .filter(n -> n % 2 == 0)
                .forEach(System.out::println);
    }
}

注意filter的意思是筛选出满足条件的项(返回true的项)。

3.3 Stream.reduce()

Stream.reduce()则是Stream的一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果。reduce()方法传入的对象是BinaryOperator接口,它定义了一个apply()方法,负责把上次累加的结果和本次的元素 进行运算,并返回累加的结果。reduce()操作需要初始化结果为指定值(这里是0)。

import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (x, y) -> x + y);  // 求 1 到 9 的和
        System.out.println(sum);
    }
}

注意reduce()是聚合方法,聚合方法会立刻对Stream进行计算。

四. 聚合操作

reduce()只是一种聚合操作,如果我们希望把Stream的元素保存到集合,例如List,因为List的元素是确定的Java对象,因此,把Stream变为List不是一个转换操作,而是一个聚合操作,它会强制Stream输出每个元素。

4.1 输出为List

Stream的每个元素收集到List的方法是调用collect()并传入Collectors.toList()对象,它实际上是一个Collector实例,通过类似reduce()的操作,把每个元素添加到一个收集器中(实际上是ArrayList)。

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("Apple", null, "Pear", null, "Orange");
        List<String> list = stream.filter(s -> s != null).collect(Collectors.toList());
        System.out.println(list);
    }
}

4.2 输出为Set

Stream<String> stream = Stream.of("Apple", null, "Pear", null, "Orange");
Set<String> set = stream.filter(s -> s != null).collect(Collectors.toSet());

4.3 输出为数组

Stream的元素输出为数组和输出为List类似,我们只需要调用toArray()方法,并传入数组的“构造方法”。注意到传入的“构造方法”是String[]::new,它的签名实际上是IntFunction<String[]>定义的String[] apply(int),即传入int参数,获得String[]数组的返回值。

List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);

4.4 输出为Map

如果我们要把Stream的元素收集到Map中,就稍微麻烦一点。因为对于每个元素,添加到Map时需要keyvalue,因此,我们要指定两个映射函数,分别把元素映射为keyvalue

import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
        Map<String, String> map = stream
                .collect(Collectors.toMap(
                        s -> s.substring(0, s.indexOf(':')),
                        s -> s.substring(s.indexOf(':') + 1)));
        System.out.println(map);
    }
}

4.5 输出为分组

Stream还有一个强大的分组功能,可以按组输出。分组输出使用Collectors.groupingBy(),它需要提供两个函数:一个是分组的key,这里使用s -> s.substring(0, 1),表示只要首字母相同的String分到一组,第二个是分组的value,这里直接使用Collectors.toList(),表示输出为List

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
        Map<String, List<String>> groups = list.stream()
                .collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
        System.out.println(groups);
    }
}

五. 其他聚合方法

除了reduce()collect()外,Stream还有一些常用的聚合方法:

  • count():用于返回元素个数;
  • max(Comparator<? super T> cp):找出最大元素;
  • min(Comparator<? super T> cp):找出最小元素。

针对IntStream、LongStream和DoubleStream,还额外提供了以下聚合方法:

  • sum():对所有元素求和;
  • average():对所有元素求平均数。

还有一些方法,用来测试Stream的元素是否满足以下条件:

  • boolean allMatch(Predicate<? super T>):测试是否所有元素均满足测试条件;
  • boolean anyMatch(Predicate<? super T>):测试是否至少有一个元素满足测试条件。

最后一个常用的方法是forEach(),它可以循环处理Stream的每个元素,我们经常传入System.out::println来打印Stream的元素:

Stream<String> s = ...
s.forEach(str -> {
    System.out.println("Hello, " + str);
});

六. 其他操作

6.1 排序

此方法要求Stream的每个元素必须实现Comparable接口。如果要自定义排序,传入指定的Comparator即可。注意sorted()只是一个转换操作,它会返回一个新的Stream

List<String> list = Stream.of("Orange", "apple", "Banana")
        .sorted(String::compareToIgnoreCase)
        .collect(Collectors.toList());
System.out.println(list);

6.2 去重

Stream.of("A", "B", "A", "C", "B", "D")
        .distinct()
        .forEach(System.out::println);

6.3 截取

截取操作常用于把一个无限的Stream转换成有限的Streamskip()用于跳过当前Stream的前N个元素,limit()用于截取当前Stream最多前N个元素,截取操作也是一个转换操作,将返回新的Stream

Stream.of("A", "B", "C", "D", "E", "F")
    .skip(2)                       // 跳过A, B
    .limit(3)                      // 截取C, D, E
    .collect(Collectors.toList()); // [C, D, E]

6.4 合并

将两个Stream合并为一个Stream可以使用Stream的静态方法concat()

Stream<String> s1 = Stream.of("A", "B", "C");
Stream<String> s2 = Stream.of("D", "E");

Stream<String> s = Stream.concat(s1, s2);
s.forEach(System.out::println);                  // [A, B, C, D, E]

6.5 flatMap

如果Stream的元素是集合,而我们希望把上述Stream转换为Stream<Integer>,就可以使用flatMap()

Stream<List<Integer>> s = Stream.of(
        Arrays.asList(1, 2, 3),
        Arrays.asList(4, 5, 6),
        Arrays.asList(7, 8, 9));

Stream<Integer> i = s.flatMap(list -> list.stream());

因此,所谓flatMap(),是指把Stream的每个元素(这里是List)映射为Stream,然后合并成一个新的Stream

┌─────────────┬─────────────┬─────────────┐
│┌───┬───┬───┐│┌───┬───┬───┐│┌───┬───┬───┐│
││ 1 │ 2 │ 3 │││ 4 │ 5 │ 6 │││ 7 │ 8 │ 9 ││
│└───┴───┴───┘│└───┴───┴───┘│└───┴───┴───┘│
└─────────────┴─────────────┴─────────────┘
                     │
                     │flatMap(List -> Stream)
                     │
                     ▼
   ┌───┬───┬───┬───┬───┬───┬───┬───┬───┐
   │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
   └───┴───┴───┴───┴───┴───┴───┴───┴───┘

6.6 并行

通常情况下,对Stream的元素进行处理是单线程的,即一个一个元素进行处理。但是很多时候,我们希望可以并行处理Stream的元素,因为在元素数量非常大的情况,并行处理可以大大加快处理速度。把一个普通Stream转换为可以并行处理的Stream非常简单,只需要用parallel()进行转换。经过parallel()转换后的Stream只要可能,就会对后续操作进行并行处理。我们不需要编写任何多线程代码就可以享受到并行处理带来的执行效率的提升。

Stream<String> s = ...
String[] result = s.parallel()                 // 变成一个可以并行处理的Stream
                   .sorted()                   // 可以进行并行排序
                   .toArray(String[]::new);
🔗

文章推荐