JAVA Stream简单原理——手写一个Stream流

2,742 阅读5分钟

前言

直到目前为止,只要是使用java8以上的版本,在对List以及Map处理过程中,几乎所有的场景都会涉及到Stream的使用。分析了一下Stream源码,发现Stream的抽象复杂度极高,为了应对广泛的使用场景而进行了高度封装,导致源码看起来非常的头疼。

所以根据Stream的思想,简单实现一个Stream,具有以下两个特性:

  • 惰性计算
  • 函数式接口

正文

首先根据自己的理解来介绍以下Stream内部特性:

惰性计算:Stream的实现应该类似于一个链表将所有的值串联起来,但是并不是传统的链表,它内部应该保存一个值,表示当前计算的结果;同时还保存了下一个值的计算方式,我们每访问一个节点的时候都会调用这个函数式接口来计算当前的值,并且跟新函数来表示下一个值。

@FunctionalInterface
public interface Function<R, T> {
    /**
     * 根据输入返回结果
     *
     * @param t 输入
     * @return 输出
     */
    R apply(T t);
}

@FunctionalInterface
public interface EvalFunction<T> {

    /**
     * stream流的强制求值方法
     * @return 求值返回一个新的stream
     * */
    StreamImpl<T> apply();
}

首先编写两个函数式接口,第一个表示根据输入返回输出,第二表示返回一个新的Stream

public interface Stream<T> {

    <R> StreamImpl<R> map(Function<R, T> mapper);


    <R, A> R collect(Collector<T,A,R> collector);

    static <T> StreamImpl<T> emptyStream() {
        return StreamImpl.<T>builder().empty(true).build();
    }
}

Stream的接口,静态方法返回一个空的流

public class StreamImpl<T> implements Stream<T> {
    private T value;
    private boolean empty = false;
    private NextValueEval<T> nextItemEval;

public static class NextValueEval<T> {
        private final EvalFunction<T> evalFunction;

        public NextValueEval(EvalFunction<T> evalFunction) {
            this.evalFunction = evalFunction;
        }

        StreamImpl<T> eval() {
            return evalFunction.apply();
        }
    }

Stream的实现类参数,value表示当前计算出的值,nextItemEval表示下一个流的计算方式,函数式接口由自己定义

public static class Builder<T> {
        private final StreamImpl<T> stream;

        public Builder() {
            this.stream = new StreamImpl<>();
        }

        public Builder<T> head(T value) {
            this.stream.value = value;
            return this;
        }

        public Builder<T> empty(boolean empty) {
            this.stream.empty = empty;
            return this;
        }

        public Builder<T> nextItemEvalProcess(EvalFunction<T> evalFunction) {
            this.stream.nextItemEval = new NextValueEval<>(evalFunction);
            return this;
        }

        public StreamImpl<T> build() {
            return stream;
        }
    }

这里使用了Builder的设计模式

  private StreamImpl<T> eval() {
        return this.nextItemEval.eval();
    }

    private boolean isEmpty() {
        return empty;
    }

这是Stream的两个方法:计算出新的流和该流是否为空


按照目前给出的方法,已经可以编写一段代码来生成一个list的流了。思路就是:下一个流的计算方式:iterator.next,流的终止: !iterator.hasNext()

 public static <T> StreamImpl<T> asStream(List<T> list) {
        return StreamImpl.<T>builder().nextItemEvalProcess(() -> getListStream(list.iterator())).build();
    }


public static <T> StreamImpl<T> getListStream(Iterator<T> iterator) {
        if (!iterator.hasNext()) {
            return Stream.emptyStream();
        }

        return StreamImpl.<T>builder()
                .head(iterator.next())
                .nextItemEvalProcess(() -> getListStream(iterator))
                .build();
    }

每一次调用nextItemEval的方法会计算出下一个流,实际上在这里每一个流都包含的是iterator,直到iterator没有元素

可以根据这个思路再编写一个整数流,大概思虑都是差不多的


接下来看一下map方法,map方法也是一个惰性求值,并不会将流遍历一遍,而是跟新nextItemEval方法。可以这样理解:将原本的nextItemEval求值方法用map方法进行代理,之中有一种代理的思想在里面

private static <R, T> StreamImpl<R> map(Function<R, T> mapper, StreamImpl<T> stream) {
        if (stream.isEmpty()) {
            return Stream.emptyStream();
        }
        R value = mapper.apply(stream.value);
        return StreamImpl.<R>builder()
                .head(value)
                .nextItemEvalProcess(() -> map(mapper, stream.eval())).build();
    }


@Override
public <R> StreamImpl<R> map(Function<R, T> mapper) {
    return StreamImpl.<R>builder().nextItemEvalProcess(() ->
            map(mapper, this.eval())).build();
    }

用mapper计算出一个新的value,再把这个value赋值给下一个流。可以理解为用这个静态的map方法将原有的stream.eval()给包装了

对于filter、flatMap等方法,同样可以利用这个思路。比如filter就是编写一个函数式接口返回一个boolean值,同map一样在内部增加判断即可


以上都是惰性求值,我们还需要一个最终方法来将流给收集起来,返回一个特定的数据结构

<R, A> R collect(Collector<T,A,R> collector);

Collector是一个高阶函数,T代表流的单个元素类型,A是中间量,R代表返回值

public interface Collector<T, A, R> {

    /**
     * 收集时,提供初始化的值
     * */
    Supplier<A> supplier();

    /**
     * A = A + T
     * 累加器,收集时的累加过程
     * */
    BiFunction<A, A, T> accumulator();

    /**
     * 收集完成之后的收尾操作
     * */
    Function<R, A> finisher();
}

@FunctionalInterface
public interface Supplier<T> {

    /**
     * 提供初始值
     * @return 初始化的值
     * */
    T get();
}

@FunctionalInterface
public interface BiFunction<R, T, U> {

    /**
     * 函数式接口
     * 类似于 z = F(x,y)
     * */
    R apply(T t, U u);
}

总结就是:

  • Supplier是获取初始值的
  • BiFunction是用于值的处理,list就是add,map就是put
  • finisher用于最后的收尾,比如说返回一个不可修改的list,使用Collections.unmodifiableList(list),也是完全没有问题的
public class Collectors {
    public static <T> Collector<T, List<T>, List<T>> toList() {
        return new Collector<T, List<T>, List<T>>() {
            @Override
            public Supplier<List<T>> supplier() {
                return ArrayList::new;
            }

            @Override
            public BiFunction<List<T>, List<T>, T> accumulator() {
                return (list, item) -> {
                    list.add(item);
                    return list;
                };
            }

            @Override
            public Function<List<T>, List<T>> finisher() {
                return list -> list;
            }
        };
    }

}

toList方法,accumulator调用list.add方法加入集合

    @Override
    public <R, A> R collect(Collector<T, A, R> collector) {
        A result = collect(collector, this.eval());
        return collector.finisher().apply(result);
    }

    public static <T, A, R> A collect(Collector<T, A, R> collector, StreamImpl<T> stream) {
        if (stream.isEmpty()) {
            return collector.supplier().get();
        }
        T value = stream.value;
        A tail = collect(collector, stream.eval());
        return collector.accumulator().apply(tail, value);
    }

这是写的第一版collect方法,一直递归到stream的结尾空stream,用supplier创建一个List,然后accumulator开始进行list的添加,最后调用收尾函数。

但是这样有缺点:

  • list的顺序相比于原有的list是反序的,因为是递归到最后开始添加
  • 递归深度太大有影响,如果元素太多,会造成栈溢出。

根据以上缺点改成循环:

public static <T, A, R> A collect(Collector<T, A, R> collector, StreamImpl<T> stream) {
        A res = collector.supplier().get();
        while (!stream.isEmpty()) {
            T value = stream.value;
            collector.accumulator().apply(res, value);
            stream = stream.eval();
        }
        return res;
    }

循环条件就是流不为空,然后调用eval去计算下一个

public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            list.add(i);
        }

        List<String> result = StreamImpl.asStream(list).map(String::valueOf)
                .collect(Collectors.toList());
        result.forEach(System.out::println);
    }

测试结果正常 (注意这个forEach是javaStream而不是自己编写的,如果有需要也可以自己编写)

总结

虽然实现的api比较少,其实只想展现一下Stream思路,同时比较复杂的并行流也没有实现,希望阅读这篇文章后能够理解函数式编程,理解流。