Java基础知识——Stream

简介

Java从8开始,不但引入了Lambda表达式,还引入了一个全新的流式API:Stream API。它位于java.util.stream包中,流提供了一种让我们可以在比集合更高的概念级别上指定计算的数据视图,Stream API的特点有,一、提供了一套新的流式处理的抽象序列;二、支持函数式编程和链式操作;三、可以表示无限序列,并且大多数情况下是惰性求值的。

与java.io的区别

Stream不同于java.io的InputStream和OutputStream,它代表的是任意Java对象的序列。两者对比如下:

java.io java.util.stream
存储 顺序读写的byte或char 顺序输出的任意Java对象实例
用途 序列化至文件或网络 内存计算/业务逻辑
与List的区别

这个Stream和List也不一样,List存储的每个元素都是已经存储在内存中的某个Java对象,而Stream输出的元素可能并没有预先存储在内存中,而是实时计算出来的,即惰性计算,对比如下:

java.util.List java.util.stream
元素 已分配并存储在内存 可能未分配,实时计算
用途 操作一组已存在的Java对象 惰性计算

它可以“存储”有限个或无限个元素。这里的存储打了个引号,是因为元素有可能已经全部存储在内存中,也有可能是根据需要实时计算出来的。Stream的另一个特点是,一个Stream可以轻易地转换为另一个Stream,而不是修改原Stream本身。最后,真正的计算通常发生在最后结果的获取,也就是惰性计算,惰性计算的特点是:一个Stream转换为另一个Stream时,实际上只存储了转换规则,并没有任何计算发生

创建Stream

创建Stream主要有以下几种方法:

  • Stream.of()
  • 基于数组或Collection
  • 基于Supplier
  • 其他方法
Stream.of()

创建Stream最简单的方式是直接用Stream.of()静态方法,传入可变参数即创建了一个能输出确定元素的Stream:

    Stream<String> stream = Stream.of("A", "B", "C", "D");
    // stream.forEach()方法相当于内部循环调用,需传入方法实现
    // 可传入符合Consumer接口的void accept(T t)的方法引用:
    stream.forEach(System.out::println);
基于数组或Collection

第二种创建Stream的方法是基于一个数组或者Collection,这样该Stream输出的元素就是数组或者Collection持有的元素,把数组变成Stream使用Arrays.strem()方法。对于Collection(List、Set、Queue等),直接调用stream()方法就可以获得Stream:

    Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
    Stream<String> stream2 = List.of("X", "Y", "Z").stream();
    stream1.forEach(System.out::println);
    stream2.forEach(System.out::println);

上述创建Stream的方法都是把一个现有的序列变为Stream,它的元素是固定的。

基于Supplier

创建Stream还可以通过Stream.generate()方法,

Stream<String> s = Stream.generate(Supplier<String> sp);

它需要传入一个Supplier对象,以下是Supplier类

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

基于Supplier创建的Stream会不断调用Supplier.get()方法来不断产生下一个元素,这种Stream保存的不是元素,而是算法,它可以用来表示无限序列。Supplier类介绍如下:

函数式接口 参数类型 返回类型 抽象方法名 描述
Supplier<T> T get 提供一个T类型的值

例如,编写一个能不断生成自然数的Supplier

   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;
	}
}

上述用一个Supplier<Integer>模拟了一个无限序列(当然受int范围限制不是真的无限大)。如果用List表示,即便在int范围内,也会占用巨大的内存,而Stream几乎不占用空间,因为每个元素都是实时计算出来的,用的时候再算。
对于无限序列,如果直接调用forEach()或者count()这些最终求值操作,会进入死循环,因为永远无法计算完这个序列,所以正确的方法是先把无限序列变成有限序列,例如,用limit()方法可以截取前面若干个元素。

其他方法

创建Stream还可以通过一些API提供的接口,直接获得Stream。例如,Files类的lines()方法可以把一个文件变成一个Stream,每个元素代表文件的一行内容,此方法对于按行遍历文本文件十分有用。:

Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))

另外,正则表达式的Pattern对象有一个splitAsStream()方法,可以直接把一个长字符串分割成Stream序列而不是数组:

Stream<String> s = p.splitAsStream("The quick brown fox jumps over the lazy dog");

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

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

map

Stream.map()是Stream最常用的一个转换方法,它把一个Stream转换为另一个Stream,即把一种操作运算,映射到一个序列的每一个元素上,例如:

Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> s2 = s.map(n -> n * n);

map()方法接收的对象是Function接口对象

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

Function的定义是:

@FunctionalInterface
public interface Function<T, R> {
    // 将T类型转换为R:
    R apply(T t);
}

Function接口定义了一个apply()方法,负责把一个T类型转换成R类型:

函数式接口 参数类型 返回类型 抽象方法名 描述
Supplier<T,R> T R apply 把一个T类型转换成R类型

利用map(),不但能完成数学计算,对于字符串操作,以及任何Java对象都是非常有用的。例如:

List.of("  Apple ", " pear ", " ORANGE", " BaNaNa ")
            .stream()
            .map(String::trim) // 去空格
            .map(String::toLowerCase) // 变小写
            .forEach(System.out::println); // 打印

通过若干步map转换,可以写出逻辑简单、清晰的代码。

filter

Stream.filter()也是Stream的常用转换方法,filter()操作就是对一个Stream的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream,例如:

	//过滤掉偶数,留下奇数
    IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
            .filter(n -> n % 2 != 0)
            .forEach(System.out::println);

filter()方法接收的对象是Predicate接口对象

@FunctionalInterface
public interface Predicate<T> {
    // 判断元素t是否符合条件:
    boolean test(T t);
}

Predicate接口定义了一个test()方法,负责判断元素是否符合条件:

函数式接口 参数类型 返回类型 抽象方法名 描述
Predicate<T> T boolean test 判断元素所给元素是否符合条件

reduce

map()和filter()都是Stream的转换方法,而Stream.reduce()则是Stream的一个聚合方法,聚合方法会立刻对Stream进行计算,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果,例如:

    int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (acc, n) -> acc + n);
    System.out.println(sum); // 45

reduce()方法传入的对象是BinaryOperator接口

@FunctionalInterface
public interface BinaryOperator<T> {
    // Bi操作:两个输入,一个输出
    T apply(T t, T u);
}

BinaryOperator接口定义了一个apply()方法,负责把上次累加的结果和本次的元素进行运算,并返回累加的结果:

函数式接口 参数类型 返回类型 抽象方法名 描述
BinaryOperator<T> T,T T apply 输入两个T,输出一个T

上述代码看上去不好理解,但我们用for循环改写一下,就容易理解了:

Stream<Integer> stream = ...
int sum = 0;
for (n : stream) {
    sum = (sum, n) -> sum + n;
}

可见,reduce()操作首先初始化结果为指定值(这里是0),紧接着,reduce()对每个元素依次调用(acc, n) -> acc + n,其中,acc(accumulator)是上次计算的结,因此,实际上这个reduce()操作是一个求和。
如果去掉初始值,我们会得到一个Optional<Integer>:

Optional<Integer> opt = stream.reduce((acc, n) -> acc + n);
if (opt.isPresent) {
    System.out.println(opt.get());
}

这是因为Stream的元素有可能是0个,这样就没法调用reduce()的聚合函数了,因此返回Optional对象,需要进一步判断结果是否存在。

输出集合

转换操作只是保存了转换规则,并不会进行计算,也不会有任何内存增长,而聚合操作则不一样,聚合操作会立刻促使Stream输出它的每一个元素,并依次纳入计算,以获得最终结果。所以,对一个Stream进行聚合操作,会触发一系列连锁反应,聚合操作是真正需要从Stream请求数据的,对一个Stream做聚合计算后,结果就不是一个Stream,而是一个其他的Java对象。

输出为List

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

Stream<String> stream = Stream.of("Apple", "", null, "Pear", "  ", "Orange");
List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
输出为数组

把Stream的元素输出为数组和输出为List类似,我们只需要调用toArray()方法,并传入数组的“构造方法”:

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

注意到传入的“构造方法”是String[]::new,它的签名实际上是IntFunction<String[]>定义的String[] apply(int),即传入int参数,获得String[]数组的返回值。

输出为Map

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

    Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
    Map<String, String> map = stream
            .collect(Collectors.toMap(
                    // 把元素s映射为key:
                    s -> s.substring(0, s.indexOf(':')),
                    // 把元素s映射为value:
                    s -> s.substring(s.indexOf(':') + 1)));
    System.out.println(map);
分组输出

Stream还有一个强大的分组功能,可以按组输出。

List<String> list = List.of("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);

分组输出使用Collectors.groupingBy(),它需要提供两个函数:一个是分组的key,这里使用s -> s.substring(0, 1),表示只要首字母相同的String分到一组,第二个是分组的value,这里直接使用Collectors.toList(),表示输出为List,上述代码运行结果如下:

{
    A=[Apple, Avocado, Apricots],
    B=[Banana, Blackberry],
    C=[Coconut, Cherry]
}

可见,结果一共有3组,按"A",“B”,"C"分组,每一组都是一个List。

其他操作

排序

对Stream的元素进行排序十分简单,只需调用sorted()方法,此方法要求Stream的每个元素必须实现Comparable接口。如果要自定义排序,传入指定的Comparator即可:

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

注意sorted()只是一个转换操作,它会返回一个新的Stream。

去重

对一个Stream的元素进行去重,没必要先转换为Set,可以直接用distinct():

List.of("A", "B", "A", "C", "B", "D")
    .stream()
    .distinct()
    .collect(Collectors.toList()); // [A, B, C, D]
截取

截取操作常用于把一个无限的Stream转换成有限的Stream,skip()用于跳过当前Stream的前N个元素,limit()用于截取当前Stream最多前N个元素:

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

截取操作也是一个转换操作,将返回新的Stream。

合并

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

Stream<String> s1 = List.of("A", "B", "C").stream();
Stream<String> s2 = List.of("D", "E").stream();
// 合并:
Stream<String> s = Stream.concat(s1, s2);
System.out.println(s.collect(Collectors.toList())); // [A, B, C, D, E]
flatMap

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

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

原理如下图:
在这里插入图片描述

并行

通常情况下,对Stream的元素进行处理是单线程的,即一个一个元素进行处理。但是很多时候,我们希望可以并行处理Stream的元素,因为在元素数量非常大的情况,并行处理可以大大加快处理速度。把一个普通Stream转换为可以并行处理的Stream非常简单,只需要用parallel()进行转换:

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

经过parallel()转换后的Stream只要可能,就会对后续操作进行并行处理。我们不需要编写任何多线程代码就可以享受到并行处理带来的执行效率的提升

其他聚合方法

除了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的元素

原创文章 69 获赞 52 访问量 29万+

猜你喜欢

转载自blog.csdn.net/qq_22136439/article/details/104573026