Jens Schauder
Jens Schauder

Reputation: 81882

Adding an element to the end of a stream for each element already in the stream

Given a function Function<T, T> f and a Stream<T> ts what is a good (nice readability, good performance) way of creating a new Stream<T> which first contains the original elements and then the elements converted by f.

One might think this would work:

Stream.concat(ts, ts.map(f));

But this doesn't work and results in an exception instead:

java.lang.IllegalStateException: stream has already been operated upon or closed

Note: that the order does matter: original elements have to come first in correct order, then the transformed elements in matching order.

Upvotes: 6

Views: 376

Answers (3)

Malte Hartwig
Malte Hartwig

Reputation: 4555

You can implement a Spliterator that wraps your source stream. Internally, you will create the "duplicate" element for each processed one, and then switch to those duplicates once the source is empty:

public class Duplicates<T> implements Spliterator<T> {
    private Spliterator<T> source;

    private Consumer<T>    addDuplicate;

    private Builder<T>     extrasStreamBuilder = Stream.builder();
    private Spliterator<T> extrasSpliterator;

    private Duplicates(Stream<T> source, UnaryOperator<T> f) {
        this.addDuplicate = t -> extrasStreamBuilder.add(f.apply(t));
        this.source = source.spliterator();
    }

    public static <T> Stream<T> of(Stream<T> source, UnaryOperator<T> f) {
        return StreamSupport.stream(new Duplicates<>(source, f), false);
    }

    @Override
    public boolean tryAdvance(Consumer<? super T> action) {
        boolean advanced = false;

        if (extrasSpliterator == null) {
            advanced = source.tryAdvance(addDuplicate.andThen(action));
        }

        if (!advanced) {
            if (extrasSpliterator == null) {
                extrasSpliterator = extrasStreamBuilder.build().spliterator();
            }
            advanced = extrasSpliterator.tryAdvance(action);
        }

        return advanced;
    }

    @Override
    public void forEachRemaining(Consumer<? super T> action) {
        if (extrasSpliterator == null) {
            source.forEachRemaining(addDuplicate.andThen(action));
            extrasSpliterator = extrasStreamBuilder.build().spliterator();
        }

        extrasSpliterator.forEachRemaining(action);
    }

    // other spliterator methods worked with default (Eclipse) implementation for the example below, but should probably delegate to source
}

public static void main(String[] args) {
    List<String> input = Arrays.asList("1", "2", "3");

    Stream<String> wrapper = Duplicates.of(input.stream(), i -> i + "0");

    wrapper.forEach(System.out::println);
}

// Output:
// 1
// 2
// 3
// 10
// 20
// 30

It might depend on your use case whether this is efficient enough in regards of memory consumption as you keep the extras in the stream builder.

The advantage over collecting and mapping ahead of your actual stream processing is that you only traverse the source once. This might be helpful when retrieving the elements takes long or the order of elements can change between streams.

You are also able to first chain some stream operations to the source before duplicating, again without having to collect the intermediate result into a collection.

Upvotes: 6

Rene Pfeuffer
Rene Pfeuffer

Reputation: 141

When it's only for the elements and not for the order (first the original items, then the modified), you could use flatMap:

Stream<T> s = ...;
Stream<T> result = s.flatMap(x -> Stream.of(x, f.apply(x));
result.forEach(System.out::println);

If the order is relevant, one could ask why you want to use streams, because you won't benefit from lazy evaluation...

Upvotes: 3

GhostCat
GhostCat

Reputation: 140427

You can't open a bottle of wine and then pass the bottle to another person and ask him to open it again.

Thus I think this is not possible by the nature of streams to do what you ask for.

You have one chain of "processing" per stream. You can't have two.

So the closest you could get at would be working from "its origin", like

Stream.concat(someList.stream(), someList.stream().map(f));

for example. And of course, when you don't have that list, you could go for:

List<Whatever> someList = ts.collect(Collectors.asList());

first.

Upvotes: 7

Related Questions