would the flatMap(..) approach have the same limitation?
The documentation for flatMap says that the stream it returns is the result of replacing each element of the old stream with the contents of the stream returned by the mapping function, when applied on that element. This wording suggests (to me) that there isn't any nesting of the streams, even though it isn't very clear about this. When the pipeline receives an element, it just needs to apply the function to it, and call forEach on the stream returned, doing whatever downstream thing it needs to do.
With concat however, there is clearly nesting. Each concat creates a Stream that has 2 parts. When you have a stream pipeline that is formed by repeated concating, downstream code will be executed for each element in the first part first, then for each element of in the second part. But since this is a heavily nested concat, one of the parts is also divided into two parts, and one of those parts also has two parts... To access the most deeply nested element, you need to go through a lot of calls.
Here's some code that illustrates this.
Suppose we have the streams a to g:
var a = Stream.of(1);
var b = Stream.of(2);
var c = Stream.of(3);
var d = Stream.of(4);
var e = Stream.of(5);
var f = Stream.of(6);
var g = Stream.of(7);
We can concat them using all three ways and print the stack trace length:
// 1
Stream.of(a, b, c, d, e, f, g).flatMap(Function.identity()).forEachOrdered(x -> {
System.out.println(new Exception().getStackTrace().length);
});
// 2
Stream.concat(Stream.concat(Stream.concat(Stream.concat(Stream.concat(Stream.concat(a, b), c), d), e), f), g)
.forEachOrdered(x -> {
System.out.println(new Exception().getStackTrace().length);
});
// 3
Stream.of(a, b, c, d, e, f, g).reduce(Stream::concat).get().forEachOrdered(x -> {
System.out.println(new Exception().getStackTrace().length);
});
(Note that you should run only one of these at a time.)
On my machine, (1) prints 13 seven times no matter how many streams I pass into of. Both (2) and (3) prints:
10
10
9
8
7
6
5
The first elements 1 and 2 are the most deeply nested, which is why they have the longest stack trace.
Unfortunately this answer is probably of little or no help whatsoever, but I did a forensics analysis of the Java Lambda Mailing list to see if I could find the cause of this design. This is what I found out.
In the beginning there was an instance method for Stream.concat(Stream)
In the mailing list I can clearly see the method was originally implemented as an instance method, as you can read in this thread by Paul Sandoz, about the concat operation.
In it they discuss the issues that could arise from those cases in which the stream could be infinite and what concatenation would mean in those cases, but I do not think that was the reason for the modification.
You see in this other thread that some early users of the JDK 8 questioned about the behavior of the concat instance method when used with null arguments.
This other thread reveals, though, that the design of the concat method was under discussion.
Refactored to Streams.concat(Stream,Stream)
But without any explanation, suddenly, the methods were changed to static methods, as you can see in this thread about combining streams. This is perhaps the only mail thread that sheds a bit of light about this change, but it was not clear enough for me to determine the reason for the refactoring. But we can see they did a commit in which they suggested to move the concat method out of Stream and into the helper class Streams.
Refactored to Stream.concat(Stream,Stream)
Later, it was moved again from Streams to Stream, but yet again, no explanation for that.
So, bottom line, the reason for the design is not entirely clear for me and I could not find a good explanation. I guess you could still ask the question in the mailing list.
Some Alternatives for Stream Concatenation
This other thread by Michael Hixson discusses/asks about other ways to combine/concat streams
To combine two streams, I should do this:
Stream.concat(s1, s2)not this:
Stream.of(s1, s2).flatMap(x -> x)... right?
To combine more than two streams, I should do this:
Stream.of(s1, s2, s3, ...).flatMap(x -> x)not this:
Stream.of(s1, s2, s3, ...).reduce(Stream.empty(), Stream::concat)... right?
If you add static imports for Stream.concat and Stream.of, the first example could be written as follows:
Stream<Foo> stream = concat(stream1, concat(stream2, of(element)));
Importing static methods with generic names can result in code that becomes difficult to read and maintain (namespace pollution). So, it might be better to create your own static methods with more meaningful names. However, for demonstration I will stick with this name.
public static <T> Stream<T> concat(Stream<? extends T> lhs, Stream<? extends T> rhs) {
return Stream.concat(lhs, rhs);
}
public static <T> Stream<T> concat(Stream<? extends T> lhs, T rhs) {
return Stream.concat(lhs, Stream.of(rhs));
}
With these two static methods (optionally in combination with static imports), the two examples could be written as follows:
Stream<Foo> stream = concat(stream1, concat(stream2, element));
Stream<Foo> stream = concat(
concat(stream1.filter(x -> x!=0), stream2).filter(x -> x!=1),
element)
.filter(x -> x!=2);
The code is now significantly shorter. However, I agree that the readability hasn't improved. So I have another solution.
In a lot of situations, Collectors can be used to extend the functionality of streams. With the two Collectors at the bottom, the two examples could be written as follows:
Stream<Foo> stream = stream1.collect(concat(stream2)).collect(concat(element));
Stream<Foo> stream = stream1
.filter(x -> x!=0)
.collect(concat(stream2))
.filter(x -> x!=1)
.collect(concat(element))
.filter(x -> x!=2);
The only difference between your desired syntax and the syntax above is, that you have to replace concat(...) with collect(concat(...)). The two static methods can be implemented as follows (optionally used in combination with static imports):
private static <T,A,R,S> Collector<T,?,S> combine(Collector<T,A,R> collector, Function<? super R, ? extends S> function) {
return Collector.of(
collector.supplier(),
collector.accumulator(),
collector.combiner(),
collector.finisher().andThen(function));
}
public static <T> Collector<T,?,Stream<T>> concat(Stream<? extends T> other) {
return combine(Collectors.toList(),
list -> Stream.concat(list.stream(), other));
}
public static <T> Collector<T,?,Stream<T>> concat(T element) {
return concat(Stream.of(element));
}
Of course there is a drawback with this solution that should be mentioned. collect is a final operation that consumes all elements of the stream. On top of that, the collector concat creates an intermediate ArrayList each time it is used in the chain. Both operations can have a significant impact on the behaviour of your program. However, if readability is more important than performance, it might still be a very helpful approach.