12

I know that Stream.concat exists (doc) to concatenate two streams. However, I have run into cases where I need to add "a few more" items to an existing stream, and then continue processing on it. In such a situation, I would have expected to be able to chain together methods like:

getStream(someArg)
  .map(Arg::getFoo)
  .concat(someOtherStreamOfFoos) // Or append, or...
  .map(...)

However, no such instance-level chainable append/concat method exists.

This isn't a question asking for solutions to this problem, or more elegant approaches (although I would of course be grateful for any other viewpoints!). Rather, I'm asking about the design factors that led to this decision. The Stream interface was, I trust, designed by some extremely smart people who are aware of the Principle of Least Astonishment - so, I must assume that their decision to omit this (to me) intuitively-obvious method signifies either that the method is an antipattern, or that it is not possible due to some technical limitation. I'd love to know the reason.

Ousmane D.
  • 54,915
  • 8
  • 91
  • 126
scubbo
  • 4,969
  • 7
  • 40
  • 71
  • 2
    It's a good question but I am trying to think of a case where making it an instance method is any better than it being `static`. for example, the code snippet you've shown can also be written as `Stream.concat(getStream(someArg).map(Arg::getFoo), someOtherStreamOfFoos).map(...)` – Ousmane D. Apr 02 '18 at 17:40
  • I suspect it wouldn't have worked well with the type system. – user2357112 Apr 02 '18 at 17:41
  • @Aominè - agreed, the effectively-same functionality is definitely still available by the existing methods. My argument is more for readability - "reading down" a Stream pipeline, with sequential filter/map/reduce operations, is much more intuitive to me than having to jump "back up" a few lines (after noticing an extra close-paren) to identify a `concat` that was declared higher up. – scubbo Apr 02 '18 at 17:45
  • 2
    It's a good question. Maybe the comment `Use caution when constructing streams from repeated concatenation` is part of it. If it were a fluent API you may be tempted to concatenate repeatedly, whereas forcing you to do e.g. `concat(s1, concat(s2, s3))` makes people less likely to do it? – Paul Boddington Apr 02 '18 at 17:46
  • 11
    I found the changeset that moves concat to a static method: http://mail.openjdk.java.net/pipermail/lambda-dev/2012-November/006887.html but no comments as to why seem to be given. Neither here: https://bugs.openjdk.java.net/browse/JDK-8015315 – Jorn Vernee Apr 02 '18 at 17:54
  • 1
    I guess `Stream`'s pipeline implementation is complicated enough as to allow more than one source for the elements. I also don't know how parallelism would play in this situation. StreamEx library has `append` and `prepend` methods, so it's doable. I'm not aware of the complexities, though. – fps Apr 02 '18 at 19:05

2 Answers2

13

I can give you one reason it wouldn't have worked.

Stream.concat is defined as

static <T> Stream<T> concat(Stream<? extends T> a,
                            Stream<? extends T> b)

You can concat a Stream<HashMap> and Stream<Map> into a Stream<Map>, or even concat a Stream<HashMap> and a Stream<TreeMap> into a Stream<Map>. To do that with an instance method, you would need to be able to declare a type parameter like <U super T>, which Java doesn't allow.

// It'd look kind of like this, if Java allowed it.
public <U super T> Stream<U> concat(Stream<? extends U> other)

Java only allows upper-bounded type parameters, not lower-bounded.

Concatenating a Stream<Something> and a Stream<SomethingElse> might seem unusual, but type inference often produces type parameters too specific to work with an instance method. For example,

Stream.concat(Stream.of(dog), animalStream)

which would require an explicit type parameter if written as

Stream.<Animal>of(dog).concat(animalStream)
user2357112
  • 260,549
  • 28
  • 431
  • 505
  • 3
    I think returning the same generic type would still be useful in most cases, like when the two streams have the same type. They could keep the less-convenient static version for the rare cases. Also, for anyone wondering _why_ `` isn't valid Java, see: https://stackoverflow.com/a/33895470/2643425 – Sean Van Gorder Apr 02 '18 at 18:24
  • 2
    If I am not mistaken, this would be possible with the chaining method as described in https://bugs.openjdk.java.net/browse/JDK-8140283? It would then alleviate the readability problem. – Koekje Apr 02 '18 at 18:25
  • 3
    This is true, but in practice it wouldn't have mattered. If the signature had been `Stream concat(Stream extends T> other)`, that would have been ok for most cases. For the rare cases when you really did want to concatenate a `Stream` and a `Stream` you could just have done `integerStream.map(a -> (Number) a).concat(doubleStream)` – Paul Boddington Apr 02 '18 at 19:07
  • @PaulBoddington: I suspect it would matter more for cases where the first argument also needs type inference, like `Stream.concat(Stream.of(dog), animalStream)`. – user2357112 Apr 02 '18 at 19:36
0

I think it is just missed functionality of Stream API.

Note that RxJava's Observable has method "concatWith" with required functionality, so your question is reasonable:

Observable<String> o1 = ...;
Observable<Integer> o2 = ...;
o1.map(s -> s.length())
  .concatWith(o2)
  ....

Java 8 has another functionality is nice to have is get another Optional if current Optional is empty, like:

Optional.ofNullable(x).orElse(anotherOptional)

What I want to say that this concat you described is possible, just not implemented in the Stream.

Dmitry Gorkovets
  • 2,208
  • 1
  • 10
  • 19
  • the use of `orElse(anotherOptional)` is wrong. Either it should be `orElse(aValue)` or the java9 introduced `or(anotherOptional)` – Lino Apr 05 '18 at 09:02