3

I am exploring the possibilities of the Java Stream API in order to find out if one could possibly replace all loop-based constructs with stream-based constructs. As an example that would probably hint to the assumption that this is actually possible, consider this:

Is it possible to use the Stream API to split a string containing words delimited by spaces into an array of strings like the following call of String.split(String) would do ?

String[] tokens = "Peter Paul Mary".split(" ");

I am aware of the fact that a stream-based solution could make itself use of the String.split(String) method like so:

Stream.of("Peter Paul Mary".split(" "))
      .collect(Collectors.toList());

or make use of Pattern.splitAsStream(CharSequence) (the implementation of which certainly uses a loop-based approach) but I am looking for a Stream-only solution, meaning something along the lines of this Haskell snippet:

words   :: String -> [String]
words s =  case dropWhile Char.isSpace s of
                  "" -> []
                  s' -> w : words s''
                        where (w, s'') = break Char.isSpace s'

I am asking this because I am still wondering if the introduction of the Stream API will lead to a profound change in the way we handle object collections in Java or just add another option to it, thus making it more challenging to maintain a larger codebase rather than to simplify this task in the long run.

EDIT: Although there is an accepted answer (see below) it only shows that its possible in this special case. I am still interested in any hints to the general case as required in the question.

GhostCat
  • 137,827
  • 25
  • 176
  • 248
Angle.Bracket
  • 1,438
  • 13
  • 29
  • 4
    @Michael Isn't "I am exploring the possibilities of the Java Stream API" the reason? – Sweeper Apr 04 '18 at 11:23
  • Why is the first question came in my mind as well ? also `Stream.of("Peter Paul Mary".split(","))` this means it will separate on commas not on spaces and you will get nothing as output. you have to use `Stream.of("Peter Paul Mary".split("//s+"))` – Rishal Apr 04 '18 at 11:23
  • 5
    `Pattern.compile(",") .splitAsStream(myString) .collect(Collectors.toList());`? btw this `.map (elem -> new String(elem))` is redundant. – Ousmane D. Apr 04 '18 at 11:25
  • What do you consider to be part of Stream API? – lexicore Apr 04 '18 at 11:31
  • @lexicore Everything in the `java.util.stream` package... maybe. – Sweeper Apr 04 '18 at 11:32
  • @Sweeper So `Collection.stream()` is not a part of Stream API? – lexicore Apr 04 '18 at 11:33
  • would a custom collector fit into java-8 stream only? – Eugene Apr 04 '18 at 11:36
  • Depending on what exactly you mean by “stream only” this is not possible. And at least streams are not suited for your job. – Ole V.V. Apr 04 '18 at 11:48
  • 2
    I think it can get difficult if you need to access indexes. How would you implement matrix multiplication or bubble sort with streams only? – Ralf Renz Apr 04 '18 at 12:59
  • Why on earth would splitting a string with streams prove that **all loop constructs** can be replaced with streams? – Michael Apr 05 '18 at 11:00
  • @Michael - that's right, it does not prove anything. It merely gives a hint. In this respect this question has not yet been answered. See the EDIT to my question. – Angle.Bracket Apr 05 '18 at 11:16
  • @Angle.Bracket It doesn't give any hint. Your question fundamentally seems to be "will streams replace for-loops?" and GhostCat's answer correctly answers that with a very emphatic '**No**'. – Michael Apr 05 '18 at 11:28
  • I am not going to provide a proof, but I would expect that all loops can potentially be replaced by some (unreadable) stream code (that uses a loop under the hood). – Ole V.V. Apr 05 '18 at 16:42

2 Answers2

7

A distinct non answer here: you are asking the wrong question!

It doesn't matter if all "loop related" lines of Java code can be converted into something streamish.

Because: good programming is the ability to write code that humans can easily read and understand.

So when somebody puts up a rule that says "we only use streams from hereon for everything we do" then that rule significantly reduces your options when writing code. Instead of being able to carefully decide "should I use streams" versus "should I go with a simple old-school loop construct" you are down to "how do I get this working with streams"!

From that point of view, you should focus on coming up with "rules" that work for all people in your development team. That could mean to empasize the use of stream constructs. But you definitely want to avoid the absolutism, and leave it to each developer to write up that code that implements a given requirement in the "most readable" way. If that is possible with streams, fine. If not, don't force people to do it.

And beyond that: depending on what exactly you are doing, using streams comes also with performance cost. So even when you can find a stream solution for a problem - you have to understand its runtime cost. You surely want to avoid using streams in places where they cost too much (especially when that code is already on your "critical path" regarding performance, like: called zillions of times per second).

Finally: to a certain degree, this is a question of skills. Meaning: when you are trained in using streams, it is much easier for you to A) read "streamish" code that others wrote and B) coming up with "streamish" solutions that are in fact easy to read. In other words: this again depends on the context you are working. The other week I was educating another team on "clean code", and my last foil was "Clean Code, Java8 streams/lambdas". One guy asked me "what are streams?" No point in forcing such a community to do anything with streams tomorrow.

GhostCat
  • 137,827
  • 25
  • 176
  • 248
  • 1
    @Eugene A) it is too long for a comment and B) it is a non answer. Surprisingly enough, these are answers sometimes too. – GhostCat Apr 04 '18 at 12:53
3

Just for fun (this is one horrible way to do it), neither do I know if this fits your needs:

List<String> result = ",,,abc,def".codePoints()
            .boxed()
            // .parallel()
            .collect(Collector.of(
                    () -> {
                        List<StringBuilder> inner = new ArrayList<>();
                        inner.add(new StringBuilder());
                        return inner;
                    },
                    (List<StringBuilder> list, Integer character) -> {
                        StringBuilder last = list.get(list.size() - 1);
                        if (character == ',') {
                            list.add(new StringBuilder());
                        } else {
                            last.appendCodePoint(character);
                        }
                    },
                    (left, right) -> {
                        left.get(left.size() - 1).append(right.remove(0));
                        left.addAll(right);
                        return left;
                    },
                    list -> list.stream()
                            .map(StringBuilder::toString)
                            .filter(x -> !x.equals(""))
                            .collect(Collectors.toList())

    ));
Eugene
  • 117,005
  • 15
  • 201
  • 306
  • Your technically correct answer was like the reason I put up my non-answer btw ;-) – GhostCat Apr 04 '18 at 12:54
  • Accepted as being a correct answer to the example mentioned in the question. But as @Eugene wrote himself, this is certainly not beautiful code. – Angle.Bracket Apr 04 '18 at 13:08
  • That doesn’t answer the question whether it is possible to replace *all* loop constructs in Java with stream-based constructs. It isn’t even an example of converting a loop to a stream construct, though this is due to the poor example of the question, as a call to `String.split` is not a loop construct at all. If we ignore the existing `split` method, a reasonable loop solution would iterate until `String.indexOf` returns `-1`, which doesn’t match this solution at all. – Holger Apr 04 '18 at 14:21
  • @Holger you're right I guess, I don't know much Haskell which would have helped here (I answered before the edit or did not notice it) :( – Eugene Apr 04 '18 at 14:25
  • @Holger to clarify things: the example I gave was rather meant to point to the _implementation_ of `String.split(String)` or `Pattern.splitAsStream(CharSequence)`: these are both realized with classical loops. By asking readers to think of solutions to split a string using only the Stream API I was hoping to find a hint to my more general question. In this respect @Eugene's answer certainly helped me because it demonstrated the possibility to use the Stream API at least in this specific case, which is why I accepted it. – Angle.Bracket Apr 04 '18 at 14:43
  • @Angle.Bracket but there are tons of other possibilities to implement this with streams. Since there is already `Pattern.splitAsStream(CharSequence)`, whose implementation is not more “loop based” than the approach of this answer, it is not clear what this answer gives you… – Holger Apr 04 '18 at 14:48
  • @Holger have a look at the (OpenJDK) implementation of `pattern.splitAsStream(CharSequence)`: it is a loop based implementation and thus very different from the answers' approach. Note that I am not preferring the latter over the former - I was just being curious if it is possible at all. And as I stated in my question, the reason for my curiosity is: if this is possible, then at what cost. And if the cost is not too high (which I more and more doubt), then maybe Java could turn into something similar to Haskell or F# and the like. – Angle.Bracket Apr 04 '18 at 15:07
  • @Angle.Bracket as you can see [here](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/util/regex/Pattern.java#Pattern.splitAsStream%28java.lang.CharSequence%29) `Pattern.splitAsStream` is implemented using an `Iterator`, [just like `CharSequence.chars`](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/lang/CharSequence.java#CharSequence.chars%28%29) which `String` inherits (in Java 8). Of course, there will be a loop within the Stream implementation iterating over it *in either case*. – Holger Apr 04 '18 at 15:45
  • @Eugene by the way, why did you use `boxed()` and `Collector.of(…)` which does not add any benefit over the three-arg `collect(…)` method of `IntStream`? When you further replace `List>` with `List`, the solution becomes much less horrible. – Holger Apr 04 '18 at 16:10
  • @Holger not using a `StringBuilder` was idiotic, agreed. But the 3 argument collector does have a finisher method, which I needed... – Eugene Apr 04 '18 at 18:53
  • You can simply chain the `.stream() .map(StringBuilder::toString) .collect(Collectors.toList())` after the three-arg `collect` method. Further, you should replace `chars()` with `codePoints()` and `.append(Character.toChars(character))` with `.appendCodePoint(character)`. That’s clean *and* efficient. – Holger Apr 05 '18 at 09:29
  • If the string has any trailing delimiters, this will produce a longer list than `String.split`. – Michael Apr 05 '18 at 10:53
  • @Michael indeed, but that is a simple `filter`... corrected – Eugene Apr 05 '18 at 11:33
  • @Eugene It's not that simple. `String.split("a,,b")` will give a list of length 3, yours a list of length 2 – Michael Apr 05 '18 at 11:36
  • @Holger every time I have to deal with String/char, a few days *after* I realize that I should have used code-pointsXXX related methods. You have indeed made it much nicer – Eugene Apr 05 '18 at 11:38