流式编程-详解

简介

流式编程是Java 8引入的新特性,它提供了一种高效、声明式的方式来处理数据集,旨在简化代码,提高可读性和可维护性

流是元素的序列,支持顺序或并行的聚合操作。在一个流上进行的操作称为流计算,每个流计算都会返回一个新的流或最终结果

流的特性:

  • 为了执行计算,每个流操作被组合到流管道中,一个流管道由数据源、中间操作、最终操作组成。
    • 数据源:数组、集合、IO管道等,
    • 中间操作:把一个流转换为另一个流,
    • 最终操作:生成一个最终结果。
  • 流是懒惰的:在数据源上的计算只有当最终操作被触发时才会开始执行

流式编程的核心概念:

  • 数据源:流的数据来源
  • 数据处理:对流中的元素进行什么样的计算
  • 惰性求值:对于流的操作是惰性的,只有当遇到终止操作时,才会触发之前设定好的操作
  • 不可变性:流是不可变的,它不会修改原始数据,每一步操作都会返回一个新的流对象,
  • 并行处理:流支持并行处理,可以利用多核处理器的优势来提高处理速度。

入门案例

需求:一个list集合,集合中存储了用户信息,计算出年龄小于20岁的所有用户的名称

代码:

public static void main(String[] args) {
    List<Person> personList = new ArrayList<>();
    personList.add(new Person("zs", 18));
    personList.add(new Person("ls", 19));
    personList.add(new Person("ww", 20));

    // 流式计算:返回年龄小于20岁的人的姓名的集合
    List<String> nameList = personList.stream()
            // 过滤,获取年龄小于20岁的用户
            .filter(person -> person.getAge() < 20)  
            // 转换,取出用户的名称
            .map(Person::getName)
            // 收集,将用户的名称收集到结果集中
            .collect(Collectors.toList());

    System.out.println("nameList = " + nameList); // [zs, ls]
}

可以看到,比起for循环,流式编程不需要使用中间变量来存储中间状态,而且基于lambda表达式的支持,代码也更加简洁。

常用操作

流的创建

从集合创建流:

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();

从数组创建流:

java
String[] array = {"a", "b", "c"};
Stream<String> stream = Arrays.stream(array);

使用静态方法创建流:

Stream<String> stream = Stream.of("a", "b", "c");

中间操作

中间操作会返回一个新的流,它们是惰性的,只有在终端操作时才会执行。

过滤(filter):筛选出符合条件的元素。

Stream<String> filteredStream = stream.filter(s -> s.startsWith("a"));

映射(map):对流中的每个元素应用一个函数。

Stream<String> upperCaseStream = stream.map(String::toUpperCase);

扁平化映射(flatMap):多集合合并,在使用时先将集合转换为流,传入给flatMap,它会把流展开,把多个流合并为一个流

List<List<String>> listOfLists = Arrays.asList(Arrays.asList("a", "b"), Arrays.asList("c", "d"));
List<String> collect = listOfLists.stream().flatMap(Collection::stream).collect(Collectors.toList());
System.out.println("collect = " + collect);    // collect = [a, b, c, d]

去重(distinct):去除流中的重复元素。

Stream<String> distinctStream = stream.distinct();

排序(sorted):对流中的元素进行排序。

Stream<String> sortedStream = stream.sorted();

终端操作

终端操作会触发流的处理,并生成一个结果。

收集(collect):将流转换为其他形式,如集合。收集有很多复杂的用法,例如将list转换为map,这里只展示一种最简单常用的,将流中的元素收集到集合中

List<String> list = stream.collect(Collectors.toList());

计数(count):返回流中元素的个数。

long count = stream.count();

遍历(forEach):对流中的每个元素执行一个动作。

stream.forEach(System.out::println);

规约(reduce):将流中的元素组合为一个结果。

Optional<String> concatenated = stream.reduce((s1, s2) -> s1 + s2);

并行计算

调用流的parallel方法,随后的计算会并行执行。

案例:

List<Person> people = Arrays.asList(
        new Person("Alice", 30),
        new Person("Bob", 25),
        new Person("Charlie", 30),
        new Person("David", 25)
);
people.stream().parallel()
        .forEach((person) -> {
            String tName = Thread.currentThread().getName();
            System.out.println(tName + " - " + person.getName())
        });

实战案例

list转map

情况1:根据id映射值

方式1:不推荐
@Test(expected = IllegalStateException.class)
public void test3_1() {
    List<Person> people = Arrays.asList(new Person(1L, "aaa", 18),
            new Person(2L, "bbb", 19),
            new Person(2L, "ccc", 20));

    Map<Long, Person> map = people.stream()
            .collect(Collectors.toMap(Person::getId, p1 -> p1));
    assert map.get(1L).getName().equals("aaa");
    assert map.get(2L).getName().equals("ccc");
}

按照这种方法,如果id有重复的话,会直接报错,源码:

public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper) {
    return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}

注意看toMap方法的第三个参数,throwingMerger(),它用于处理一个key有多个值的情况,它的处理方式是抛异常。

方式2:推荐
@Test
public void test3() {
    List<Person> people = Arrays.asList(new Person(1L, "aaa", 18),
            new Person(2L, "bbb", 19),
            new Person(2L, "ccc", 20));

    Map<Long, Person> map = people.stream()
            .collect(Collectors.toMap(Person::getId, p1 -> p1, (existing, replacement) -> existing));
    assert map.get(1L).getName().equals("aaa");
    assert map.get(2L).getName().equals("ccc");
}

根据id映射值,一个id对应一个实例,注意要处理key重复的逻辑。如果id有重复,在代码中要做出处理,避免报错。

情况2:根据key分组,元素值直接聚合

@Test
public void test5() {
    List<Person> people = Arrays.asList(
            new Person(1L, "Alice", 30),
            new Person(2L, "Bob", 25),
            new Person(3L, "Charlie", 30),
            new Person(4L, "David", 25));

    Map<Integer, List<Person>> map = people.stream().collect(Collectors.groupingBy(Person::getAge));
    assert map.size() == 2;
}

作用:根据年龄进行分组,把Person对象中的name收集到结果集中

情况3:根据key分组,值需要进一步计算才可以放到结果集中

@Test
public void test5_1() {
    List<Person> people = Arrays.asList(
            new Person(1L, "Alice", 30),
            new Person(2L, "Bob", 25),
            new Person(3L, "Charlie", 30),
            new Person(4L, "David", 25));

    Map<Integer, List<String>> ageToNamesMap = people.stream()
            .collect(Collectors.groupingBy(
                    Person::getAge,
                    Collectors.mapping(Person::getName, Collectors.toList())
            ));
    assert ageToNamesMap.size() == 2;
}

作用:根据年龄进行分组,把Person对象中的name收集到结果集中

情况3:根据key分组,值需要进一步聚合才可以放到结果集中

@Test
public void test5_2() {
    List<Person> people = Arrays.asList(
            new Person(1L, "Alice", 30),
            new Person(2L, "Bob", 25),
            new Person(3L, "Charlie", 30),
            new Person(4L, "David", 25));

    Map<Integer, Optional<String>> ageToLongestNameMap = people.stream()
            .collect(Collectors.groupingBy(
                    Person::getAge,
                    Collectors.mapping(Person::getName,
                            Collectors.reducing((s1, s2) -> s1.length() > s2.length() ? s1 : s2))
            ));

    assert ageToLongestNameMap.size() == 2;
    assert ageToLongestNameMap.get(25).orElse("").equals("David");
}

作用:根据年龄进行分组,每个分组内只要名字最长的name

map 遍历map的过程中修改map

可以通过迭代器来完成这种操作

@Test
public void test6() {
    // 造数据
    List<Person> people = Arrays.asList(
            new Person(1L, "Alice", 30),
            new Person(2L, "Bob", 25),
            new Person(3L, "Charlie", 30),
            new Person(4L, "David", 25));
    Map<Integer, List<Person>> map = people.stream().collect(Collectors.groupingBy(Person::getAge));
    Iterator<Map.Entry<Integer, List<Person>>> it = map.entrySet().iterator();
    while (it.hasNext()) {
        Map.Entry<Integer, List<Person>> entry = it.next();
        Integer age = entry.getKey();
        if (age == 25) {
            it.remove();  // 遍历map的过程中移除键值对,使用迭代器来完成
            // entry.setValue() 还可以通过entry来修改值对象
        }
    }
    assert map.size() == 1;
}

list 集合排序

情况1:根据日期倒序排序

方式1:原始的写法
@Test
public void test7() {
    List<Employee> people = Arrays.asList(
            new Employee(1L, "aaa", new Date(System.currentTimeMillis() - 60000)),
            new Employee(2L, "bbb", new Date(System.currentTimeMillis() - 10000)),
            new Employee(3L, "ccc", new Date(System.currentTimeMillis() - 40000)));
    people.sort(new Comparator<Employee>() {
        @Override
        public int compare(Employee o1, Employee o2) {
            long t1 = o1.getCreateTime().getTime();
            long t2 = o2.getCreateTime().getTime();
            return t1 == t2 ? 0 : t1 > t2 ? -1 : 1;
        }
    });
    assert people.get(0).getId().equals(2L);
}
方式2:使用Comparator提供的默认方法
@Test
public void test7_1() {
    List<Employee> people = Arrays.asList(
            new Employee(1L, "aaa", new Date(System.currentTimeMillis() - 60000)),
            new Employee(2L, "bbb", new Date(System.currentTimeMillis() - 10000)),
            new Employee(3L, "ccc", new Date(System.currentTimeMillis() - 40000)));

    people.sort(Comparator.comparing(Employee::getCreateTime).reversed());
    assert people.get(0).getId().equals(2L);
}

还可以使用流式API中提供的sort算子,案例中这种方法,是直接修改原集合,使用sort算子,是生成一个新集合

情况2:多字段排序

    @Test
    public void test7_2() {
        List<Employee> people = Arrays.asList(
                new Employee(1L, "bbb", new Date(System.currentTimeMillis() - 60000)),
                new Employee(3L, "aaa", new Date(System.currentTimeMillis() - 10000)),
                new Employee(2L, "aaa", new Date(System.currentTimeMillis() - 40000)));

        // 使用 Comparator 组合实现多字段排序,在第一个字段有序的情况下,第二个字段有序,默认升序排序
        Comparator<Employee> comparator = Comparator.comparing(Employee::getName)
                .thenComparing(Employee::getId);

        // 使用 Streams API 进行排序
        people.sort(comparator);

        assert people.get(0).getName().equals("aaa");
        assert people.get(0).getId().equals(2L);
    }

解析二维表中的数据

需求:假设先在有一个集合,集合中存储了学校中每个学生的成绩单,现在想要把数据组织成Map<班级名称, Map<学生id, 学生成绩>>的格式。

实现:

// 成绩单
List<ScoreaDTO> scoreList = new ArrayList<>();
// 存储结果
Map<String, Map<String, List<ScoreDTO>>> scoreMap = new HashMap<>();
for (ScoreaDTO dto : scoreList) {
    scoreMap
        .computeIfAbsent(dto.getClassName(), k -> new HashMap<>())
        .computeIfAbsent(dto.getStudentId(), k -> new ArrayList<>())
        .add(dto);
}

list 移除指定范围的元素

案例:

list.subList(0, 5).clear();

这里使用了subList方法中提供的功能,它的内部会调用重写的removeRange方法。

函数式接口

由于流式编程大量使用到了函数式接口,所以在这里也学习一下函数式接口

函数式接口:Java8引入的新特性,函数式接口是只包含一个抽象方法的接口,lambda表达式或方法引用的返回值,就是一个函数式接口的实例,这是Java函数式编程的基础。

计算 Function BiFunction UnaryOperator BinaryOperator

Function:

  • 功能:将输入对象转换为输出对象。接收一个参数,返回一个结果
  • 方法:R apply(T t)

案例:

Function<String, Integer> stringLength = String::length;
int length = stringLength.apply("Hello"); // 5

BiFunction:

  • 功能:接受两个参数,并返回一个结果。
  • 方法:R apply(T t, U u)

案例:

BiFunction<String, String, String> concat = String::concat;
String result = concat.apply("Hello, ", "World!"); // Hello, World!

UnaryOperator :对单个操作数进行操作,并返回与输入类型相同的结果。 继承自 Function

案例:

UnaryOperator<Integer> square = x -> x * x;
int result = square.apply(5); // 25

BinaryOperator:对两个操作数进行操作,并返回与输入类型相同的结果。继承自 BiFunction

案例:

BinaryOperator<Integer> sum = Integer::sum;
int result = sum.apply(3, 5); // 8

判断 Predicate BiPredicate

Predicate:

  • 功能:用于测试对象是否满足某个条件。
  • 方法:boolean test(T t)

案例:

Predicate<String> isEmpty = String::isEmpty;
boolean result = isEmpty.test(""); // true

BiPredicate:

  • 功能:测试两个参数是否满足某个条件。
  • 方法:boolean test(T t, U u)

案例:

BiPredicate<String, String> isEqual = String::equals;
boolean result = isEqual.test("test", "test"); // true

提供者 Supplier

Supplier:

  • 功能:提供对象的生成,不接受参数,返回一个结果
  • 方法:T get()

案例:

Supplier<Double> randomValue = Math::random;
double value = randomValue.get(); // 生成一个随机数

消费者 Consumer BiConsumer

Consumer:

  • 功能:对给定的对象执行某种操作(例如,打印),接收一个参数,没有返回值
  • 方法:void accept(T t)

案例:

Consumer<String> print = System.out::println;
print.accept("Hello, World!"); // 输出: Hello, World!

BiConsumer:

  • 功能:对两个参数执行某种操作。接受两个参数,没有返回值
  • 方法:void accept(T t, U u)

案例:

BiConsumer<String, Integer> printWithAge = 
        (name, age) -> System.out.println(name + " is " + age + " years old.");
printWithAge.accept("Alice", 30); // 输出: Alice is 30 years old.

流式编程-源码

流式编程要做的事情,就是从数据源中生成一个流,然后编排在这个流上需要进行的计算,再执行这些计算,获取结果。计算功能由算子提供,算子分为中间算子和结果算子,中间算子输入一个流,输出一个流,结果算子输入一个流,输出一个结果。中间算子的执行是惰性的,只有在遇到结果算子时才会触发中间算子的执行。

将入门案例中的链式调用拆分为普通调用:

// 需求:现在有一个集合,挑出集合中可以被转换为数字的字符串,然后将他们转换为数字,然后排序
List<String> list = Arrays.asList("一", "3", "2", "四");

Stream<String> stream = list.stream();
Stream<String> filterStream = stream.filter(s -> s.matches("\\d+"));
Stream<Integer> mapStream = filterStream.map(Integer::parseInt);
Stream<Integer> sortedStream = mapStream.sorted();
List<Integer> collect = sortedStream.collect(Collectors.toList());

System.out.println("collect = " + collect); // [2, 3]

流式编程的基本执行步骤:

  • 用户通过流式编程提供的api,获取一个流,每个流都有自己的数据源,它可以是集合,也可以是io流
  • 在这个流上编排计算,filter、map、sorted、collect,都是算子。在流上进行的编排最终会生成一个双向链表。以当前案例为例,调用stream方法,实际上是返回双向链表的头节点,然后在头节点上调用map方法,返回一个无状态的计算节点,它会指向之前的头节点,同时让头节点指向自己,以此类推,直到最后的collect方法,它会返回一个终端节点,这也是双向链表的尾节点。用户不需要知道这个双向链表,只需要操作流对象即可。
  • 双向链表的头节点存储数据源,中间节点存储用户设置好的计算,用户的计算通常是一个lambda表达式,中间节点会存储一个代表lambda表达式的匿名对象,尾结点也会负责存储计算,同时,它还会触发evaluate函数,这函数会遍历到双向链表的头节点,获取数据源,遍历数据源中的元素,一个元素依次执行完流中的所有计算后,下一个元素再执行,直到所有的元素全部执行完成,然后获取结果。

从功能的角度分析,可以将流式编程的源码分为几个部分:

  • 面向用户的api:Stream接口,面向用户的api,提供了流式编程的基本功能,创建流、编排计算、获取结果
  • 数据源:Spliterator接口,它是迭代器的增强,可以把数据源分割为几个部分,方便并行计算
  • 算子:
    • Head:流中的头节点,存储数据源,严格来讲它不是一个算子,但它是所有计算的开始
    • StatelessOp:无状态的算子,调用filter、map方法,返回的就是它,因为这些计算不需要存储状态,输入一个元素,输出一个元素即可
    • StatefulOp:有状态的算子,例如,调用sorted方法时返回的算子,它的计算需要存储状态。
    • TerminalOp:调用collect方法时返回的算子,它代表一个终端操作,会返回一个结果,它的内部有三个实例:
      • supplier:返回一个容器,
      • accumulator:将流中的元素添加到容器中的方法,
      • combiner:冲突合并,将两个容器合并成一个。
  • evaluate函数:算子的计算流程

面向用户的api Stream接口

Stream接口:提供了流式编程的基本功能,创建流、编排计算、获取结果,它继承了BaseStream,BaseStream中定义了更加复杂的功能,例如并行计算。

public interface Stream<T> extends BaseStream<T, Stream<T>> {
    Stream<T> filter(Predicate<? super T> predicate);  // 过滤
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);  // 映射
    // 省略代码
}

BaseStream:

public interface BaseStream<T, S extends BaseStream<T, S>>
        extends AutoCloseable {
    boolean isParallel(); // 判断是否是并行计算
    S parallel();    // 开启并行计算
    // 省略代码
}

数据源 Spliterator

Java 8 引入的一个新的接口。Spliterator可以看作是Iterator的一个高级版本,它不仅能够遍历元素,还能够对元素进行分割,从而方便数据的并行处理。

案例:

public static void main(String[] args) {
    List<String> list = Arrays.asList("a", "b", "c", "d", "e", "f");
    // 获取 Spliterator
    Spliterator<String> spliterator = list.spliterator();
    // 使用 Spliterator 进行遍历,元素被消费
    while (spliterator.tryAdvance(System.out::print)) { }

    // 在这里暂不演示分割功能,因为这篇文章中不涉及流的并行计算
}

算子

在源码中,一个算子,对应双向链表中的一个节点,依据不同的计算功能,有不同类型的节点,这些节点对外提供统一的抽象。

算子的整体架构

AbstractPipeline:定义了双向链表中节点的基本架构,负责维护当前节点的前一个节点、后一个节点、节点在链表中的下标,定义了evaluate函数,它会触发整个流式计算的执行

abstract class AbstractPipeline<E_IN, E_OUT, S extends BaseStream<E_OUT, S>>
        extends PipelineHelper<E_OUT> implements BaseStream<E_OUT, S> {

    private final AbstractPipeline sourceStage;

    // 前一个节点
    private final AbstractPipeline previousStage;
    // 下一个节点
    private AbstractPipeline nextStage;
    // 节点的深度
    private int depth;
  
    // 数据源,只有头结点才会使用这个实例
    private Spliterator<?> sourceSpliterator;

    // 是否开启并行计算
    private boolean parallel;
    // 省略代码
}

ReferencePipeline:继承了AbstractPipeline,实现了Stream接口中定义的算子,针对普通的java对象进行计算,如果是包装类,有对应的类,例如DoublePipeline,计算数据类型是double的流,实际上函数式api中也有这样的优化。

abstract class ReferencePipeline<P_IN, P_OUT>
        extends AbstractPipeline<P_IN, P_OUT, Stream<P_OUT>>
        implements Stream<P_OUT>  {
  
    // 实现Stream接口中定义的算子
    public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
        Objects.requireNonNull(mapper);
        return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
                                     StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
            @Override
            Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
                return new Sink.ChainedReference<P_OUT, R>(sink) {
                    @Override
                    public void accept(P_OUT u) {
                        downstream.accept(mapper.apply(u));
                    }
                };
            }
        };
    }
    // 省略代码
}

Head:ReferencePipeline的子类,同时它也是ReferencePipeline的内部类,它是流中的头节点,存储了流的数据源。

static class Head<E_IN, E_OUT> extends ReferencePipeline<E_IN, E_OUT> {
    // 存储数据源
    Head(Supplier<? extends Spliterator<?>> source,
         int sourceFlags, boolean parallel) {
        super(source, sourceFlags, parallel);
    }

    // 省略代码
}

StatelessOp:ReferencePipeline的子类,同时它也是ReferencePipeline的内部类,它是流中的无状态计算节点,在流的内部,它是输入一个元素,计算,然后输出一个元素,不做任何额外的事情,

abstract static class StatelessOp<E_IN, E_OUT>
        extends ReferencePipeline<E_IN, E_OUT> {
    // 参数1 upstream,是当前节点的上一个节点
    StatelessOp(AbstractPipeline<?, E_IN, ?> upstream,
                StreamShape inputShape,
                int opFlags) {
        super(upstream, opFlags);
        assert upstream.getOutputShape() == inputShape;
    }

    @Override
    final boolean opIsStateful() {
        return false;
    }
}

StatefulOp:ReferencePipeline的子类,同时它也是ReferencePipeline的内部类,它是流中的有状态计算节点,它需要存储中间状态,例如排序操作,这个节点会把所有的元素都存储起来,排好序,再把元素逐个传送给下游节点。

abstract static class StatefulOp<E_IN, E_OUT>
        extends ReferencePipeline<E_IN, E_OUT> {
    
    // 参数1 upstream,是当前节点的上一个节点
    StatefulOp(AbstractPipeline<?, E_IN, ?> upstream,
               StreamShape inputShape,
               int opFlags) {
        super(upstream, opFlags);
        assert upstream.getOutputShape() == inputShape;
    }
}

TerminalOp:终端算子,比较重要的是声明了两个抽象方法,分别表示流的串行执行和并行执行

interface TerminalOp<E_IN, R> {
    // 流的并行执行
    default <P_IN> R evaluateParallel(PipelineHelper<E_IN> helper,
                                      Spliterator<P_IN> spliterator) {
        if (Tripwire.ENABLED)
            Tripwire.trip(getClass(), "{0} triggering TerminalOp.evaluateParallel serial default");
        return evaluateSequential(helper, spliterator);
    }

    // 流的串行执行
    <P_IN> R evaluateSequential(PipelineHelper<E_IN> helper,
                                Spliterator<P_IN> spliterator);
}

算子的工具类:

  • SortedOps:工具类,用于创建支持排序功能的节点,排序功能的节点继承了StatefulOp,在父类中实现节点的基本功能,存储节点的上下游,在子类中存储节点的状态。

  • ReduceOps:工具类,用于创建终端节点TerminalOp。TerminalOp中本身没有定义什么,重要的是收集器,它会使用收集器来收集元素。

收集器:

  • Collector:收集器,用于收集流中的元素,它定义了三个方法,supplier,返回一个容器,accumulator,将流中的元素添加到容器中的方法,combiner,冲突合并,将两个容器合并成一个。
public interface Collector<T, A, R> {
    // 返回一个容器
    Supplier<A> supplier();
    // 将流中的元素添加到容器中的方法
    BiConsumer<A, T> accumulator();
    // 冲突合并,将两个容器合并成一个
    BinaryOperator<A> combiner();
}
  • Collectors:为Collector准备的工具类,提供各种聚合流中元素的方式,例如,把流中的元素收集到一个集合中、把流中的元素根据id分组收集到一个map中

Sink

在进行计算时,流中的节点会被包装到一个sink实例中,Sink是Consumer的子接口,Consumer是函数式接口中的消费者接口,接收一个元素,产生一个副作用,不返回结果。Sink接口在Consumer接口上做的扩展,是begin和end方法,Consumer接口中只有一个accept方法,begin和end分别在accept的前后执行,提供了更加丰富的功能。

interface Sink<T> extends Consumer<T> {
    // begin方法
    default void begin(long size) {}

    // end方法
    default void end() {}
  
    // 最重要的是继承自Consumer的accept方法

    // Sink接口的抽象实现,用于创建链式的Sink实例,提供了begin和end方法的默认实现,
    // 在默认实现中,它们会调用下一个节点的begin、end方法
    static abstract class ChainedReference<T, E_OUT> implements Sink<T> {
        // 持有下一个节点的实例,下一个节点也是sink类型的
        protected final Sink<? super E_OUT> downstream;

        public ChainedReference(Sink<? super E_OUT> downstream) {
            this.downstream = Objects.requireNonNull(downstream);
        }

        @Override
        public void begin(long size) {
            downstream.begin(size);
        }

        @Override
        public void end() {
            downstream.end();
        }

        @Override
        public boolean cancellationRequested() {
            return downstream.cancellationRequested();
        }
    }
}

evaluate方法

整体计算流程:evaluate函数,流式编程的计算流程,它被定义在核心抽象类AbstracePipeline中。

  • evaluate函数接收终端节点作为参数,在源码中,遇到终端节点后,就会把它作为参数调用evaluate函数,所以说,终端节点触发整个计算流程
  • evaluate函数会从终端节点开始向前遍历,直到头节点,在这个过程中,它会把节点包装到sink实例中,这一步对于无状态节点没有什么,但是对于有状态节点,例如排序节点,会很有帮助。从头结点开始,遍历数据源中的元素,一个元素依次执行流中的所有计算节点,先执行每个节点上的begin方法,然后执行每个节点上的accept方法,最后执行每个节点上的end方法,一次元素执行完成,下一个元素再执行。直到所有元素执行完成,获取结果。

节点的计算:在了解了算子的整体架构、流的计算过程之后,再单独学习某些算子的计算方式。因为算子的代码结构很复杂,如果不了解整体流程,很难意识到某个部分的作用,这些部分分的很散。这里以map节点、排序节点、终端节点为例进行学习。

map节点:用户调用map函数,返回一个StatelessOp实例,一个无状态计算节点,

public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
    Objects.requireNonNull(mapper);
    // 调用map函数时的返回,参数1,this,是map节点的上一个节点,
    return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
                                 StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
        // 重写opWrapSink函数,这个函数在evaluate函数中被触发执行,负责将当前节点包装到sink实例中,
        // 参数sink在构造方法中会被赋值给downstream,它是当前节点的下一个节点的实例,也当前节点执行完成后,
        // 触发下一个节点的执行,就是在这里实现的。
        @Override
        Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
            return new Sink.ChainedReference<P_OUT, R>(sink) {
                @Override
                public void accept(P_OUT u) {
                    downstream.accept(mapper.apply(u));
                }
            };
        }
    };
}

sorted函数:排序节点,

// 省略前面的方法调用,直接看创建排序节点的核心函数

// 要点1:排序节点继承了ReferencePipeline中的StatefulOp,说明排序节点是一个有状态节点,
// 而且,上下游节点的实例维护在父类中。
private static final class OfRef<T> extends ReferencePipeline.StatefulOp<T, T> {
    /**
     * Comparator used for sorting
     */
    private final boolean isNaturalSort;
    private final Comparator<? super T> comparator;
    
    // 要点2:构造方法中,调用父类的构造方法,维护和上游节点的联系,这个时候还没有下游节点,
    // 类似的,也是由它的下游节点来创建和它的链接
    OfRef(AbstractPipeline<?, T, ?> upstream, Comparator<? super T> comparator) {
        super(upstream, StreamShape.REFERENCE,
              StreamOpFlag.IS_ORDERED | StreamOpFlag.NOT_SORTED);
        this.isNaturalSort = false;
        this.comparator = Objects.requireNonNull(comparator);
    }

    // 要点2:opWrapSink方法,在evaluate函数中被触发,将排序节点包装到sink实例中
    @Override
    public Sink<T> opWrapSink(int flags, Sink<T> sink) {
        Objects.requireNonNull(sink);
        // If the input is already naturally sorted and this operation
        // also naturally sorted then this is a no-op
        if (StreamOpFlag.SORTED.isKnown(flags) && isNaturalSort)
            return sink;
        else if (StreamOpFlag.SIZED.isKnown(flags))
            return new SizedRefSortingSink<>(sink, comparator);
        else
            return new RefSortingSink<>(sink, comparator);
    }
    // 省略代码
}

// 要点3:sink实例中的计算逻辑
private static final class RefSortingSink<T> extends AbstractRefSortingSink<T> {
    private ArrayList<T> list;
    RefSortingSink(Sink<? super T> sink, Comparator<? super T> comparator) {
        super(sink, comparator);
    }
    // 在begin方法中,创建容器,这个容器将会存储数据源中的所有元素
    @Override
    public void begin(long size) {
        if (size >= Nodes.MAX_ARRAY_SIZE)
            throw new IllegalArgumentException(Nodes.BAD_SIZE);
        list = (size >= 0) ? new ArrayList<T>((int) size) : new ArrayList<T>();
    }

    // accept方法,向容器中添加元素。注意,在begin、accept方法中不会调用下游节点的任何方法
    @Override
    public void accept(T t) {
        list.add(t);
    }

    // end方法,排序后,调用下游节点的begin方法、accept方法、end方法
    @Override
    public void end() {
        list.sort(comparator);
        downstream.begin(list.size());
        if (!cancellationWasRequested) {
            list.forEach(downstream::accept);
        }
        else {
            for (T t : list) {
                if (downstream.cancellationRequested()) break;
                downstream.accept(t);
            }
        }
        downstream.end();
        list = null;
    }
}

终端节点:这里以Collectors.toList为例,它创建了一个终端节点,将流中的元素收集到集合中。

// 第一步:Collectors.toList方法:返回一个收集器collector
public static <T>
Collector<T, ?, List<T>> toList() {
    // 要点是这三个参数:参数1,supplier,返回一个容器,参数2,accumulator,
    // 将流中的元素添加到容器中的方法,参数3,combiner,冲突合并,将两个容器合并成一个。
    // 参数4暂不讨论
    return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, 
                                List::add,
                               (left, right) -> { left.addAll(right); return left; },
                               CH_ID);
}

// 第二步:根据收集器创建一个TerminalOp。
public static <T, I> TerminalOp<T, I>
makeRef(Collector<? super T, I, ?> collector) {
    // 要点1:收集器中的三个实例
    Supplier<I> supplier = Objects.requireNonNull(collector).supplier();
    BiConsumer<I, ? super T> accumulator = collector.accumulator();
    BinaryOperator<I> combiner = collector.combiner();

    // 要点2:局部内部类,这是封装终端节点的sink实例类,类中的begin、accept、end方法,
    // 分别依赖收集器中的三个实例
    class ReducingSink extends Box<I>
            implements AccumulatingSink<T, I, ReducingSink> {
        @Override
        public void begin(long size) {
            state = supplier.get();
        }
        @Override
        public void accept(T t) {
            accumulator.accept(state, t);
        }
        @Override
        public void combine(ReducingSink other) {
            state = combiner.apply(state, other.state);
        }
    }
    // 要点3:返回终端节点的实例,注意makSink方法,它返回上面局部内部类的实例,也就是sink,
    // 所有的计算都在sink中。
    return new ReduceOp<T, I, ReducingSink>(StreamShape.REFERENCE) {
        @Override
        public ReducingSink makeSink() {
            return new ReducingSink();
        }
        @Override
        public int getOpFlags() {
            return collector.characteristics().contains(Collector.Characteristics.UNORDERED)
                   ? StreamOpFlag.NOT_ORDERED
                   : 0;
        }
    };
}

总结

上面的源码描述了流式编程的整体结构和某些执行细节,在这里做一个总结:

  • 用户调用stream方法创建一个流
  • 通过流对象,调用算子,编排要执行的计算
  • 每调用一个算子,就会创建一个节点,最后节点之间依据调用顺序组成一个双向链表,并且节点上存储了要进行的计算,计算通常是一个lambda表达式,节点实际上存储了一个匿名内部类的实例。
  • 算子分为中间算子和结果算子,中间算子只存储计算,结果算子会触发流计算的执行,所以说流是惰性的,只有在遇到终端算子的时候才会启动计算
  • 流的计算在evaluate函数中,结果算子也是通过调用这个函数来触发计算执行。
  • evaluate函数的计算过程:
    • 在evaluate函数中,它会从链表的尾结点遍历到头结点,在这个过程中,把链表上的每一个节点都封装到一个Sink实例中,节点和Sink是对应的,不同节点有不同功能的Sink实例。
    • Sink继承自Consumer接口,是一个功能更强大的消费者接口,它提供了三个方法:begin、accept、end
    • 遍历数据源中的所有数据,从第一个元素开始,依次执行流中的所有计算,先执行所有的begin方法,在执行所有的accept方法,在执行所有的end方法,这个元素对应的最终结果会被存储到结果容器中,再执行第二个元素,以此类推,直到遍历完所有元素,获取最终结果即可。
  • 获取evaluate函数的计算结果,这就是流式编程的计算结果。

在这里我从代码结构的角度分析了流式编程的执行过程,读者可以先把整体流程debug一遍,然后再来看这篇文章,也许会有启发。流式编程还有许多复杂的细节,包括流的特性、并行计算、复杂的算子,这里不涉及。

参考

  • https://www.cnblogs.com/throwable/p/15371609.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值