忠 ju

导航

 

熟悉 ES6 的开发者,肯定对数组的一些方法不是很陌生:mapfilter 等。在对一组对象进行统一操作时,利用这些方法写出来的代码比常规的迭代代码更加的简练。在 C♯ 中,有 LINQ 来实现。那么在 Java 中有这样的操作吗?答案是有的,Java8 中引入了大量新特性,其中一个就是 Java 的流式 API。

在 Java 8 中,流(Stream)与迭代器类似,都是用来对集合内的元素进行某些操作。它们之间最大的差别,是对迭代器的每个操作都会即时生效,而对流的操作则不是这样。流的操作有两种,中间操作和终止操作。对于中间操作并不会立即执行,只有当终止操作执行时,前面的中间操作才会一并执行(称之为惰性求值)。对于某些复杂操作,流的效率会比传统的迭代器要高。

注意:本文所讲述的“流”不是 XXXInputStreamXXXOutputStream

预备知识:lambda 表达式、Functional Interface

Functional Interface

在 Java8 中,新加入了一个注解:@FunctionalInterface,用于标记一个接口是函数接口(即有且只有一个方法(不包括那些有默认实现的方法和标记为 static 的方法))。一个典型的例子就是 Java 中用于多线程的 Runnable 接口:

@FunctionalInterface
public interface Runnable {
    void run();
}

另外一个例子来自于 Java8 中预定义的一些接口(位于 java.util.function 包下)

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
    
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
    
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
    
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

如果自己定义函数式接口,@FunctionalInterface 注解是可选的,只要接口内除静态方法和有默认实现的方法之外有且只有一个方法,那么这个接口就被认为是 Functional Interface。

lambda 表达式

lambda 表达式是 Java 8 中新引进的语法糖,主要作用是快速定义一个函数(或一个方法)。其基本语法如下:

(参数列表) -> { 表达式内容 }

其中参数列表内,每个参数的类型是可选的,如果参数列表内没有参数,或参数不止一个时,需要用 () 进行占位。

lambda 表达式的主要作用,就是用于简化代码。熟悉 Java GUI 的读者知道,之前要给一个控件添加事件响应的时候,我们通常是使用匿名内部类进行处理:

button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        // 这里处理事件响应的代码
    }
});

显然,这种写法是比较麻烦的,我们观察上面的代码,可以看到 ActionListener 中只有一个方法。控件的 addActionListener 实际上接受的是一个方法作为参数,事件发生时调用该方法作为响应。lambda 表达式的作用就是用于快速定义方法,于是可以对上面的方法改写成如下形式

button.addActionListener(e -> {
    // 处理事件响应
})

可以看到,引入 lambda 表达式后,整个方法都变得十分简洁。这就是 lambda 表达式的作用。

基本使用

打开流

可以用如下方法打开一个 Stream

  1. 使用 Collection 子类的 stream()(串行流)或 parallelStream()
  2. 使用 Arrays.stream() 方法为数组创建一个流
  3. 使用 Stream.of() 方法创建流
  4. 使用 Stream.iterate() 方法创建流
  5. 使用 Stream.generate() 方法创建流

其中前三种创建的流是有限流(里面的元素数量是有限个,因为创建该流的集合内元素数量也是有限的),后两种创建的流是无限流(里面的元素是由传入的参数进行生成的,具体可参阅 API 文档

对流进行操作

前文说过,流的操作有两种:中间操作和终止操作。辨别这两种操作的方法很简单:观察这些操作的返回值。如果方法的返回值是 Stream<T> 说明操作返回的是流自身,可以进行下一步操作,这一操作为中间操作,反之则为终止操作,终止操作结束后流即失效,想再次使用则需要创建新的流。

下面列举一些(至少我比较经常用到的)一些流的操作

操作 描述
<R> Stream<R> map(Function<? super T, ? extends R> mapper) 将流里面的每个元素通过 mapper 转换为另一个元素,并生成一个对应类型的流
Stream<T> filter(Predicate<? super T> predicate) 从流里挑出所有符合 predicate 条件的所有元素,并放入一个新的流中
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper) 将流里面的每个元素“展开”,形成一个新的流(通常用于展开嵌套的 List 或数组(把矩阵转换为数组之类的))
Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
常见的应用场景:求和。简单来说就是对流内的每个元素进行一次操作,最后得到一个结果
<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner)
<R, A> collect<Collector<? super T, A, R> collector
常见的应用场景:把流中的元素收集到一个 List
boolean allMatch(Predicate<? super T> predicate)
boolean anyMatch(Predicate<? super T> predicate)
判断流中是否所有元素(存在元素)满足 predicate 判断条件

以上仅展示了部分常用操作,其余操作可参见 Stream 类的 API 文档,另外不要被 API 的参数吓到。这些参数实际上大部分是来自于 java.util.function 的接口,且均为前文所说的 Functional Interface,所以实际使用时,我们都是传递 lambda 表达式给参数。

举例

对于选择题来说,其选项可以由以下结构表示

class Question {
    String body;
    List<Option> options;
    
    // 省略 getter/setter
}

class Option {
    String answer;
    boolean right;
    
    // 省略 getter/setter
}

假如我们有一个选择题的题库,要往里面添加一道选择题,要求在插入前要进行判断,说每个题目必须有至少一个正确答案,则可以这样写:

boolean isValidQuestion(Question question) {
    return question.getOptions.stream().anyMatch(option -> option.isRight());
}

再举一个例子,已知 Date 类有一个 toInstant() 方法可以将 Date 转化为 Instant,现有一个 List<Date> 的变量 dates,想将其转化为 List<Instant> 类型,可以这样写:

dates.stream().map(Date::toInstant).collect(Collectors.toList());

目前我遇到的操作大致就这些,之后遇到实际的例子会继续添加到本文。

posted on 2019-10-07 20:22  忠ju  阅读(669)  评论(0编辑  收藏  举报