Java8 新特性 —— Stream 流式编程

2年前 (2022) 程序员胖胖胖虎阿
255 0 0

本文部分摘自 On Java 8

流概述

集合优化了对象的存储,大多数情况下,我们将对象存储在集合是为了处理他们。使用流可以帮助我们处理对象,无需迭代集合中的元素,即可直接提取和操作元素,并添加了很多便利的操作,例如查找、过滤、分组、排序等一系列操作。

流的一个核心好处是:它使得程序更加短小并且易于理解,当结合 Lambda 表达式和方法引用时,会让人感觉自成一体。总而言之,流就是一种高效且易于使用的处理数据的方式。

观察下面的例子:

public class Randoms {
    
    public static void main(String[] args) {
        new Random(47)	// 创建 Random 对象,并给一个种子
            .ints(5, 20)	// 产生一个限定了边界的随机整数流
            .distinct()	// 使流中的整数不重复
            .limit(7)	// 取前7个元素
            .sorted()	// 排序
            .forEach(System.out::println);	// 根据传递给它的函数对流中每个对象执行操作
    }
}

通过上面的示例,我们可以发现流有如下特点:

  1. 流本身不存储元素,并且不会改变源对象,相反,它会返回一个持有结果的新流
  2. 流可以在不使用赋值或可变数据的情况下对有状态的系统建模
  3. 流是一种声明式编程风格,它声明想要做什么,而非指明如何做
  4. 流的迭代过称为内部迭代,你看不到迭代过程,可读性更强
  5. 流是懒加载的,它会等到需要时才执行

流创建

创建流的方式有很多,下面逐个介绍:

1. Stream.of()

通过 Stream.of() 可以很容易地将一组元素转化为流

Stream.of(new Bubble(1), new Bubble(2), new Bubble(3)).forEach(System.out::println);
Stream.of("a", "b", "c", "d", "e", "f").forEach(System.out::print);
Stream.of(3.14159, 2.718, 1.618).forEach(System.out::println);

2. stream()

每个集合也可以通过调用 stream() 方法来产生一个流

List<Bubble> list = Arrays.asList(new Bubble(1), new Bubble(2), new Bubble(3));
list.stream().forEach(System.out::print);
Set<String> set = new HashSet<>(Arrays.asList("a", "b", "c", "d", "e", "f"));
set.stream().forEach(System.out::print);

3. Stream.generate()

使用 Stream.generate() 搭配 Supplier<T> 生成 T 类型的流

Stream.generate(Math::random).limit(10).forEach(System.out::print);

4. Stream.iterate()

Stream.iterate() 产生的流的第一个元素是种子,然后把种子传递给方法,方法的运行结果被添加到流,并作为下次调用 iterate() 的第一个参数

Stream.iterate(0, n -> n + 1).limit(10).forEach(System.out::print)

使用 Stream.generate()Stream.iterate() 生成的无限流一定要用 limit() 截断

5. Stream.builder()

使用建造者模式创建一个 builder 对象,然后将创建流所需的多个信息传递给它,最后 builder 对象执行创建流的操作

Stream.Builder<String> builder = Stream.builder();
builder.add("a");
builder.add("b");
...
builder.build();	// 创建流
// builder.add("c")	// 调用 build() 方法后继续添加元素会产生异常

6. Arrays.stream()

Arrays 类中有一个名为 stream() 的静态方法用于把数组转换成流

Arrays.stream(new double[] {3.14159, 2.718, 1.618}).forEach(System.out::print);
Arrays.stream(new int[] {1, 3, 5}).forEach(System.out::print);
Arrays.stream(new long[] {11, 22, 44, 66}).forEach(System.out::print);
// 选择一个子域
Arrays.stream(new int[] {1, 3, 5, 7, 15, 28, 37}, 3, 6).forEach(System.out::print);

最后一次 stream() 的调用有两个额外的参数,第一个参数告诉 stream() 从数组的哪个位置开始选择元素,第二个参数告知在哪里停止

7. IntStream.range()

IntStream 类提供 range() 方法用于生成整型序列的流,编写循环时,这个方法会更加便利

IntStream.range(10, 20).sum();	// 求得 10 - 20 的序列和
IntStream.range(10, 20).forEach(System.out::print);	// 循环输出 10 - 20

8. 随机数流

Random 类被一组生成流的方式增强了,可以生成一组随机数流

Random rand = new Random(47);
// 产生一个随机流
rand.ints().boxed();
// 控制上限和下限
rand.ints(10, 20).boxed();
// 控制流的大小
rand.ints(2).boxed();
// 控制流的大小和界限
rand.ints(3, 3, 9).boxed();

Random 类除了能生成基本类型 int,long,double 的流,使用 boxed() 操作会自动把基本类型包装为对应的装箱类型

9. 正则表达式

Java8 在 java.util.regex.Pattern 中新增了一个方法 splitAsStream(),这个方法可以根据传入的公式将字符序列转化为流

Pattern.compile("[.,?]+").splitAsStream("a,b,c,d,e").forEach(System.out::print);

中间操作

中间操作具体包括去重、过滤、映射等操作,作用于从流中获取的每一个对象,并返回一个新的流对象。

1. 跟踪和调试

peek() 操作的目的是帮助调试,它提供了一种对流中所有元素操作的方法,同时提供一个消费函数,对流中元素进行操作,并返回一个新流。一般不建议这样做,更多的用途应该是无修改地查看流中的元素

Stream.of("a b c d e".split(" ")).map(w -> w + " ").peek(System.out::print);

2. 流元素排序

sorted() 可以帮助我们实现对流元素的排序,如果不使用默认的自然排序,则需要传入一个比较器,也可以把 Lambda 函数作为参数传递给 sorted()

Stream.of("a b c d e".split(" ")).sorted(Comparator.reverseOrder())
    .map(w -> w + " ").peek(System.out::print);

3. 移除元素

distinct() 可用于消除流中的重复元素

new Random(47).ints(5, 20).distinct().limit(7).forEach(System.out::println);

filter(Predicate) 将元素传递给过滤函数,若结果为 true,则保留元素

// 检测质数
Stream.iterate(2, n -> n + 1).filter(i -> i % 2 ==0)
    .limit(10).forEach(System.out::print)

4. 应用函数到元素

map(Function) 将函数操作应用到输入流的元素,并将返回值传递到输出流

Arrays.stream(new String[] {"12", "23", "34"}).map(s -> "[" + s + "]")
    .forEach(System.out::print)

另外还有 mapToInt(ToIntFunction)mapToLong(ToLongFunction)mapToDouble(ToDoubleFunction),操作和 map(Function) 相似,只是结果流为各自对应的基本类型

如果在将函数应用到元素的过程中抛出了异常,此时会把原始元素放到输出流

5. 组合流

使用 flatMap() 将产生流的函数应用在每个元素上,然后将产生每个流都扁平化为元素

Stream.of(1, 2, 3).flatMap(i -> Stream.of("hello" + i)).forEach(System.out::println);

另外还有 flatMapToInt(Function)flatMapToLong(Function)flatMapToDouble(Function),操作和 flatMap() 相似,只是结果元素为各自对应的基本类型

Optional 类

如果在一个空流中尝试获取元素,结果肯定是得到一个异常。我们希望可以得到友好的提示,而不是糊你一脸 NullPointException。Optional 的出现就是为了解决臭名昭著的空指针异常

一些标准流操作返回 Optional 对象,因为它们不能保证预期结果一定存在,包括:

  • findFirst()

    返回一个包含第一个元素的 Optional 对象,如果流为空则返回 Optional.empty

  • findAny()

    返回包含任意元素的 Optional 对象,如果流为空则返回 Optional.empty

  • max()min()

    返回一个包含最大值或者最小值的 Optional 对象,如果流为空则返回 Optional.empty

  • reduce(Function)

    将函数的返回值包装在 Optional 中

1. 便利函数

Optional 类本质上是一个容器对象,所谓容器是指:它可以保存类型 T 的值,也可以保存一个 null。此外,Optional 提供了许多有用的方法,可以帮助我们不用显示地进行空值检测:

  • ifPresent()

    是否有值存在,存在放回 true,否则返回 false

  • ifPresent(Consumer)

    当值存在时调用 Consumer,否则什么也不做

  • orElse(otherObject)

    如果值存在则直接返回,否则生成 otherObject

  • orElseGet(Supplier)

    如果值存在则直接返回,否则使用 Supplier 函数生成一个可替代对象

  • orElseThrow(Supplier)

    如果值存在则直接返回,否则使用 Supplier 函数生成一个异常

下面是对 Optional 的一个简单应用

class OptionalBasics {
    
    static void test(Optional<String> optString) {
        if(optString.isPresent())
            System.out.println(optString.get()); 
        else
            System.out.println("Nothing inside!");
    }
    
    public static void main(String[] args) {
        test(Stream.of("Epithets").findFirst());
        test(Stream.<String>empty().findFirst());	// 生成一个空流
    }
}

2.创建 Optional

当我们需要在自己的代码中加入 Optional 时,可以使用下面三个静态方法:

  • empty()

    生成一个空 Optional

  • of(value)

    将一个非空值包装到 Optional 里

  • ofNullable(value)

    针对一个可能为空的值,为空时自动生成 Optional.empty,否则将值包装在 Optional 中

3. Optional 对象操作

当我们的流管道生成 Optional 对象,下面三个方法可以使得 Optional 能做更多后续操作:

  • filter(Predicate)

    对 Optional 中的内容应用 Predicate 并将结果返回。如果 Optional 不满足 Predicate,将 Optional 转化为空 Optional 。如果 Optional 已经为空,则直接返回空 Optional

  • map(Function)

    如果 Optional 不为空,应用 Function 于 Optional 中的内容,并返回结果,否则直接返回 Optional.empty

  • flatMap(Function)

    一般应用于已生成 Optional 的映射函数,所以 flatMap() 不会像 map() 那样将结果封装在 Optional 中

终端操作

终端操作将获取流的最终结果,至此我们无法再继续往后传递流。可以说,终端操作总是我们在使用流时所做的最后一件事

1. 数组

当我们需要得到数组类型的数据以便于后续操作时,可以使用下述方法产生数组:

  • toArray()

    将流转换成适当类型的数组

  • toArray(generetor)

    生成自定义类型的数组

2. 循环

常见的如 forEach(Consumer),另外还有 forEachOrdered(Consumer),保证按照原始流的顺序操作。第二种形式仅在引入并行流时才有意义。所谓并行流是将流分割为多个,并在不同的处理器上分别执行。由于多处理器并行操作的原因,输出的结果可能会不一样,因此需要用到 forEachOrdered(Consumer)

3. 集合

在这里我们只是简单介绍一下常见的 Collectors 示例,实际上它还有一些非常复杂的实现。大多数情况下,java.util.stream.Collectors 中预设的 Collector 就能满足我们的需求

  • collect(Collector)

    使用 Collector 收集流元素到结果集合中

  • collect(Supplier, BiConsumer, BiConsumer)

    第一个参数创建一个新的结果集合,第二个参数将下一个元素收集到结果集合中,第三个参数用于将两个结果集合合并起来

4. 组合

组合意味着将流中所有元素以某种方式组合为一个元素

  • reduce(BinaryOperator)

    使用 BinaryOperator 来组合所有流中的元素。因为流可能为空,其返回值为 Optional

  • reduce(identity, BinaryOperator)

    功能同上,但是使用 identity 作为其组合的初始值。因此如果流为空,identity 就是结果

看一段代码示例:

Stream.generate(Math::random).limit(10)
	.reduce((fr0, fr1) -> fr0.size < 50 ? fr0 : fr1).ifPresent(System.out::println);

返回的结果是 Optional 类型,Lambda 表达式中的第一个参数 fr0 是 reduce 中上一次调用的结果,而第二个参数 fr1 是从流传递过来的值

5. 匹配

allMatch(Predicate)

如果流的每个元素提供给 Predicate 都返回 true ,结果返回为 true。在第一个 false 时,则停止执行计算

anyMatch(Predicate)

如果流的任意一个元素提供给 Predicate 返回 true ,结果返回为 true。在第一个 true 是停止执行计算

noneMatch(Predicate)

如果流的每个元素提供给 Predicate 都返回 false 时,结果返回为 true。在第一个 true 时停止执行计算

6. 查找

findFirst()

返回第一个流元素的 Optional,如果流为空返回 Optional.empty

findAny(

返回含有任意流元素的 Optional,如果流为空返回 Optional.empty

7. 信息

count()

流中的元素个数

max(Comparator)

根据所传入的 Comparator 所决定的最大元素

min(Comparator)

根据所传入的 Comparator 所决定的最小元素

8. 数字流信息

average()

求取流元素平均值

max()min()

数值流操作无需 Comparator

sum()

对所有流元素进行求和

版权声明:程序员胖胖胖虎阿 发表于 2022年10月1日 上午8:48。
转载请注明:Java8 新特性 —— Stream 流式编程 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...