
Java 8: Replace traditional for loops with IntStreams - mariushe
http://www.deadcoderising.com/2015-05-19-java-8-replace-traditional-for-loops-with-intstreams/
======
cousin_it
Maybe a heretical opinion here, but I like writing loops much more than using
higher order functions like map/fold/filter, even though I have quite a bit of
experience with ML family languages by now. These are my reasons:

1) The automatic parallelization opportunities of HOFs don't happen in
practice nearly as often as you'd expect. Sorry, but it's true. Haskell's
"map" doesn't parallelize automatically.

2) Loops are a single construct you learn once and apply everywhere, while
HOFs are a huge and bewildering zoo.

3) Loops can do many things that HOFs cannot. You can do break/continue/return
in the loop body. You can iterate over two collections at once, skipping
elements of one collection depending on what you see in the other. And so on.

4) Loops scale to from simple use cases to complicated ones gradually and
continuously. With HOFs, when your use case changes slightly (e.g. you add a
counter), you often need to go all the way up and use a different HOF.

5) Loops make the time and space complexity much more obvious. With HOFs, you
often get nasty surprises. For example, see the tail-recursive vs non-tail-
recursive implementations of "map" in OCaml, or the foldr vs foldl vs foldl'
situation in Haskell.

6) Loops are much easier to understand from a machine point of view. For HOFs,
you need a language with closures, and in some cases GC as well. Loops, on the
other hand, can be done in C.

In a nutshell, loops are easier to learn, easier to read, easier to write, and
easier to execute.

~~~
michaelfeathers
The problem I have with loops is that they are like fly-paper. Once you have a
loop iterating over a collection it's tempting to add more and more to it,
mixing responsibilities in run-on code. This isn't theoretical, I see code
like this all of the time. It's nearly impossible to abuse HOFs that way.

~~~
cousin_it
Yeah, I agree. "Fly-paper" sounds a bit judgmental though. For a more neutral
metaphor, we could say that HOFs are like Lego bricks, and loops are like
clay. Both have their benefits and drawbacks.

I've been fascinated by HOFs for many years, but my personal projects are
filled with "loop abuse" and mixed responsibilities, all soft and malleable,
just the way I like it. If I had to dismantle half the thing just to change
the color of one brick, I'd give up pretty quickly.

~~~
michaelfeathers
That sort of change is easy - at least you have bricks. Clay just oozes all
over itself.

Whatever works for you. If it's just your code, you have enough control to
keep loops from going bad. Large multi-programmer projects seem far more prone
to that problem.

------
Monkeyget
The next step is to realize that you can use streams to communicate between
processes. Stream parallelization is used not just to speed things up but as
an easier way to write massively parallel programs.

Instead of bug inducing mutexes and semaphores, you write a series of threads
who are only able to communicate between themselves through streams. Each
thread possesses input streams to receive data from other threads and output
streams to talk to other threads. Each thread is idle until it receives an
input from one of the streams. It then performs some processing which can
include sending messages to output streams before going back to sleep. A
system may possess a large number of threads and

I wrote a pacman this way where interactive object was a thread: the pacman,
ghosts, bonuses,... Even the score was in its own thread. Since the game was
real time there was a special clock process to generate the ticks to advance
through each frame of the game.

Except for the initial wiring up and flow control (when the emitter of the
stream is faster than the receiver), the system is easy to reason with and
debug. By looking at the stream you can get a high level of visualization.

I guess the next step after that is to realize that you can apply this to the
entire system and replace messy one-to-one ESB RPC calls with something like
Event Sourcing.

~~~
visarga
Standard unix programming with streams and pipes is quite functional-ish. The
auto-pausing of pipes and lazy infinite streams it makes possible are quite
useful.

------
adamc
I was thinking "this is cool" \-- a more functional style of iteration for
Java. But then it occurred to me that now you will need to know yet another
style of writing the same thing. That brought up (bad) memories of Perl, where
the problem of "there's more than one way to do it" was that you had to be
comfortable with _reading_ all of them. (And, much as I love Lisps, that's the
problem with macros in Lisp -- they create a semantic burden on new readers of
the code.)

~~~
vitalyd
One of the hotly debated aspects of java is that even with java 8 additions
the language is fairly simple and there really aren't that many ways to
express the same thing.

~~~
sacado2
Hmm... Java's reference manual is about 800 pages long. That is close to Ada's
reference manual, and Ada is quite a hard to grok language, and its reference
very exhaustive, by design. It is bigger than C++14's ISO reference, which is
fun considering Java was marketed as "much simpler than C++".

Overall, Java is not an arcane language, but it has many, many gotchas and
mixing new concepts with old design decisions only increases the number of
gotchas.

~~~
pron
The spec is long because it's very, very detailed (probably more than any
other language spec out there). Also, Java doesn't allow any undefined
behavior under any circumstance. It is much, much simpler than C++.

~~~
lmm
There are also lots of awkward edge cases where Java specifies something just
as complex as a more general language feature, that's then only used in one
tiny case. E.g. Java has a specification of type inference - that's used only
for anonymous classes. And another, more limited form of inference that's
applied to generics. Java has a syntax for union types - that can only be used
in catch clauses - and generic type bounds have their own parallel syntax and
rules. Language constructs are inconsistent (braces are mandatory around the
body of a try or a function, but not an if or while). A lot of parts of the
specification need special-casing for primitive types and/or arrays, which
most languages handle in a more unified way (e.g. accessing the length of an
array via reflection is unlike any other method or field access).

~~~
pron
So what? A language's complexity is measured (by developers) as the mental
effort required to learn and read it -- not by the regularity of the grammar.
Java is a relatively very simple language (for a statically typed one) -- even
with its corner cases. It's certainly more complex than C and a little more
than Go, but it's much simpler than C++, C#, Scala, Ada, Rust, Haskell, OCaml
and pretty much every other statically typed language out there with more than
1000 users.

~~~
lmm
These aren't just theoretical grammar issues. Every beginner struggles with
when you use == and when you use .equals(), or the differences between casting
primitives and casting objects. I've witnessed even very experienced
developers get confused reading generic bounds, or fail to take advantage of
multi-catch or generics inference. All I can say for your comparisons is that
I disagree with many of them.

~~~
pron
> Every beginner struggles with when you use == and when you use .equals(), or
> the differences between casting primitives and casting objects.

I actually agree with those points (though Java is still very simple), but
there's an obvious reason: that's the exact same behavior as in C++ (unless
you start playing with operator overloading), and any other behavior would
have looked surprising or strange to people coming over from C/C++, which was
basically everyone who learned Java in its first 10 years. The same goes for
the fallthrough switch statement.

Generic bounds indeed completely go against the language's design goals, but
failing to take advantage of multicatch or generic inference (or lambda
inference) is not a problem[1]. Java is a language designed for ease of
reading (it says so right in its design documents), and for large
teams/project, so code is made to look uniform, with all "advanced" features
being local and obvious to anyone reading them. The goal was to make every new
developer on your team (people working on large projects move around a lot)
immediately able to read the code and not have to learn the team's DSL or
coding style. Those were the problems that plagued C++ (alas, they only became
apparent years after C++ had been in widespread use, and cost the industry
billions), and Java very successfully avoided. Go has adopted the same design
philosophy.

[1]: Not that it's important, but every Java IDE would automatically suggest
the shortened syntax. Java IDEs even automatically convert loops to streams.

------
c-rack
Interesting approach. It would be nice to see some benchmarks of IntStream vs.
traditional loop.

I would expect that IntStreams are slower if it does not use the parallel
execution, see:
[https://stackoverflow.com/questions/22658322/java-8-performa...](https://stackoverflow.com/questions/22658322/java-8-performance-
of-streams-vs-collections)

And even parallel execution might be tricky:
[http://zeroturnaround.com/rebellabs/java-parallel-streams-
ar...](http://zeroturnaround.com/rebellabs/java-parallel-streams-are-bad-for-
your-health/)

~~~
Karunamon
A coworker and I tested this out when the announcement was originally made, at
least in the use case of iterating a massive array. (Insert 100M trues and one
false at the end, and find me the false)

Code is here:
[https://gist.github.com/Karunamon/abc6483ac1d08f6cc137](https://gist.github.com/Karunamon/abc6483ac1d08f6cc137).

The result was that streams were roughly 4x slower.

Still, I really like some of the new constructs that Java is getting - they
make the language a bit more expressive, lack of which has always been my main
gripe with the language.

~~~
RandomBK
Write code to be as clear and expressive as possible, then optimize for
performance when you know performance is a problem. This is why I don't mind
the performance cost of new language features like this. 90% of the time it
won't matter, and you can optimize the other 10.

~~~
sacado2
These constructs don't always make code any easier to understand / maintain. I
have played a lot with NetBeans' feature that automatically translate loops
with their "functional" equivalent. Sometimes the code was so clever I
couldn't understand what was happening.

Sure, code is more compact, but compactness is not an end in itself.

~~~
justinhj
Compactness is more of a side effect and can be a detrimental one. Personally
I find that this kind of code is more declarative of intent than the more
imperative for loop. By using particular functional tools for the job you're
avoiding any possibility of a bug in your looping construct and making your
purpose explicit.

I also like how it removes boiler plate to handle different container types
making it easier to switch to different ones.

------
btilly
Iterators are an interesting addition to Java 8, but a bigger change is what
they had to do to enable iterators to exist.

What they did was allow interfaces to provide default methods. They then added
stream() and parallelStream() default methods to the Collection interface to
generate streams and start all of the iterator goodness.

The result is that in Java 8, an interface now behaves like a Ruby mixin. It
is a way to do multiple inheritance in a language that officially does single
inheritance.

I'm sure there will be some disasters before the Java community settles on
best practices for this feature. :-)

------
michaelfeathers
The idea that you can replace nearly every loop with functional constructs
really clicked for me when I saw that you can use zip in Haskell and each_cons
in Ruby to see each element and its successor at the same time.

[10,11,2,3,4,5].each_cons(2).map {|curr,succ| succ - curr } => [1,-9,1,1,1]

~~~
gd1
Or just:

    
    
      (-)':(10 11 2 3 4 5) 
    

...in K.

Or:

    
    
      (-) prior (10 11 2 3 4 5)
    

..in Q, if you feel more comfortable with words than symbols.

------
peter303
Just write functional programs in a legible manner for understandability. I've
seen people write five functions on a single line and takes me a while to
figure out what is going on. They should be on separate lines with a possible
tail comment.

------
tsmarsh
I know backward compatibility with Java 5 is important for the clojure world,
but I would love to see streams and invoke dynamic implemented in a "Clojure
2.0".

I would love to do it myself, if someone would pay me to do it :)

~~~
WalterGR

        I know backward compatibility with Java 5 is
        important for the clojure world...
    

Why is that? (I googled but didn't find anything relevant.)

~~~
djpowell
Since Clojure 1.6, Java 6 is the minimum requirement.

I'm sure there is a discussion somewhere, but I expect that there are quite a
few people who run Java on commercial J2EE app servers and the like which
don't officially support newer JDKs.

------
danceswthmngmnt
Liking that java is getting nice now. But they need to get type inference next
too.

~~~
RyanZAG
Seems to be an unpopular opinion, but I don't like or want type inference. All
that happens is instead of seeing

    
    
      int result = someFunc()
    

you see

    
    
      val result = someFunc()
    

Is anything really gained other than saving a couple obvious (and compiler
checked) keystrokes? And you've lost the ability to see the type of each
variable locally in the function. It's never seemed very useful to me.

~~~
unwind
For your example, sure.

But it's not uncommon (in my opinion) to write stuff like

    
    
        EnumMap<FooEnum, BarClass> map = new EnumMap<FooEnum, BarClass>(FooEnum.class);
    

which to me ought to read something like

    
    
        var map = new EnumMap<FooEnum, BarClass>();
    

Of course, I don't do a lot of Java programming these days and perhaps my
first example is simply broken due to ignorance.

When I wrote something very similiar to that last week, I failed to figure out
how to avoid repeating the type signature, and was annoyed.

~~~
michaelt
It's not quite as short as you'd like, but since Java 7 came out in 2011, it's
been possible to write:

    
    
      EnumMap<FooEnum, BarClass> map = new EnumMap<>(FooEnum.class);
    

using the "diamond operator" [1]. In fact, my IDE even provides automatic
hints to make use of it!

With that said, I do think java has too much pro-long-method-and-variable-name
culture.

[1] [http://www.javaworld.com/article/2074080/core-java/jdk-7--
th...](http://www.javaworld.com/article/2074080/core-java/jdk-7--the-diamond-
operator.html)

------
hoare
cool article! parallel reminds me of C#'s Parallel.For/ForEach loop. Does
anyone have experience of the performance of streams vs conventional for
loops?

~~~
vitalyd
Performance has been briefly mentioned in this thread; if you google you'll
also see some empirical results. But really the best you can hope for, all
else equal, is same perf as for loop. The JIT compilers have been taught about
loop optos for a long time now, and the best you can hope for is the JIT
removes the extra abstraction when using streams, but it can be brittle.

------
innguest
Java needs to come out of the Ruby closet.

------
jebblue
Not only do I wish for for loops to remain I would like to see the magical
looking lambda crap removed.

