preface

Up to now, with java8 and above, almost every scenario involved the use of a Stream during List and Map processing. An analysis of Stream source code shows that Stream abstraction is extremely complex and is highly encapsulated for a wide range of usage scenarios, resulting in the source code looking very painful.

So, according to the Stream idea, simply implement a Stream with the following two characteristics:

  • The inertia calculation
  • Functional interface

The body of the

The following internal Stream features are described in terms of my own understanding:

Lazy calculation: The implementation of a Stream should be like a linked list that concatenates all the values, but instead of a traditional linked list, it should hold a value inside that represents the result of the current calculation. It also saves the calculation of the next value. Each time we visit a node, we will call this functional interface to calculate the current value and represent the next value with a new function.

@FunctionalInterface
public interface Function<R.T> {
    /** * returns the result ** based on input@paramT input *@returnO * /
    R apply(T t);
}

@FunctionalInterface
public interface EvalFunction<T> {

    /** * stream mandatory evaluation method *@returnEvaluation returns a new stream * */
    StreamImpl<T> apply(a);
}
Copy the code

Write two functional interfaces, the first to return output from input and the second to return a new 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(a) {
        return StreamImpl.<T>builder().empty(true).build(); }}Copy the code

The static method returns an empty 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(a) {
            returnevalFunction.apply(); }}Copy the code

Stream implementation class parameters, value represents the current calculated value, nextItemEval represents the calculation of the next Stream, the functional interface is defined by itself

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

        public Builder(a) {
            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(a) {
            returnstream; }}Copy the code

The design pattern of Builder is used here

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

    private boolean isEmpty(a) {
        return empty;
    }

Copy the code

These are the two methods of Stream: figuring out the new Stream and whether the Stream is empty


With the method presented so far, it is already possible to write a piece of code to generate a stream of lists. Iterator. next, the end of the stream:! 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();
    }
Copy the code

Each call to nextItemEval computes the next stream, and in this case each stream contains an iterator until the iterator has no elements

You can write a stream of integers along the same lines


Next, look at the Map method, which is also an inert evaluation and does not iterate over the stream, but rather follows the new nextItemEval method. The map method is used to proxy the original nextItemEval evaluation method. There is a kind of proxy idea in it

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();
    }
Copy the code

The mapper calculates a new value and assigns that value to the next stream. This can be interpreted as wrapping the original stream.eval() with the static map method

The same idea can be applied to methods such as filter and flatMap. Filter, for example, is written to a functional interface that returns a Boolean value, just like map, adding judgments internally


This is all lazy evaluation, and we need a final method to collect the stream and return a specific data structure

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

Collector is A higher-order function where T represents the single element type of the stream, A is the intermediate quantity, and R represents the return value

public interface Collector<T.A.R> {

    /** * When collecting, provide the initialized value ** /
    Supplier<A> supplier(a);

    /** * A = A + T * accumulator, collection of the accumulation process ** /
    BiFunction<A, A, T> accumulator(a);

    /** * The final operation after the collection is complete ** /
    Function<R, A> finisher(a);
}

@FunctionalInterface
public interface Supplier<T> {

    /** * provides the initial value *@returnThe initialized value * */
    T get(a);
}

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

    /** ** function interface * similar to z = F(x,y) ** /
    R apply(T t, U u);
}
Copy the code

The summary is:

  • Supplier retrieves the initial value
  • BiFunction is used to process values, list is add, map is PUT
  • Finisher used for the final finishing, such as returning an immutable list, use the Collections. UnmodifiableList (list), is also no problem
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() {
                returnlist -> list; }}; }}Copy the code

ToList method, accumulator calls list.add method to join the collection

    @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);
    }
Copy the code

This is the first version of the collect method, which recurses to an empty stream at the end of the stream. An Accumulator creates a List with the supplier.

But there are disadvantages:

  • The order of the list is in reverse order compared to the original list, because the list is added recursively at the end
  • Too much recursion makes a difference, and if there are too many elements, stack overflows.

According to the shortcomings of the above change to cycle:

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;
    }
Copy the code

The loop condition is that the stream is not empty and eval is called to evaluate the next one

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);
    }
Copy the code

The test results are fine (note that this forEach is javaStream and not written by yourself, you can write it yourself if you need to)

conclusion

Although the API implementation is relatively small, in fact, I just want to show the Stream idea, at the same time, more complex parallel streams are not implemented, I hope that after reading this article can understand functional programming, understand the flow.