Don’t get me wrong – I love Java. The first programming book I bought of my own free will (university reading lists don’t count) was the original ‘Java in a Nutshell’ book. I compared this side by side with ‘Essential COM’ by Don Box and it was immediately clear which direction I wanted my career to go. Java has provided hours of pleasure, put clothes on my children and pays the mortgage. So it is with a heavy heart that I have come to the conclusion that Java 8 is a bridge too far :-(
To see why lets consider the following problem. We have as input an array of Strings and need to output an array of characters. The array of characters should be created by extracting the contents of each string and then conjoining them. The image below illustrates what we are trying to achieve.
Fans of functional programming will immediately realise that this is a textbook case for flatMap. We use flatmap when:
- We have a list of type ‘A’
- Given an ‘A’ we can generate a list of ‘B’
- What we want is a single list of ‘B’ elements, ignoring where they came from
So in this case we need a function / lambda that can extract an array of characters from a string and give us the following intermediate result. Its signature will be String ⇒ char []
.
flatMap
will then join together the intermediate results for us. That is the reason for using flatMap
rather than map
.
Here is our idealised solution written in Scala:
object Program {
def main(args : Array[String]) {
val data = Array("super","cala","fragil","istic","expy","ali","dotious")
val result = data.flatMap(_.toCharArray)
for (c <- result) {
printf(" %s", c);
}
}
}
I’ve used the abbreviated lambda syntax, but you could also write it in the more verbose style:
object Program {
def main(args : Array[String]) {
val data = Array("super","cala","fragil","istic","expy","ali","dotious")
val result = data.flatMap(s => s.toCharArray)
for (c <- result) {
printf(" %s", c);
}
}
}
Either way we get the expected result:
s u p e r c a l a f r a g i l i s t i c e x p y a l i d o t i o u s
In C# the equivalent solution is almost as nice:
class Program {
private static void Main(string[] args) {
var data = new string[] {"super", "cala", "fragil", "istic",
"expy", "ali", "dotious"};
var result = data.SelectMany(s => s.ToCharArray());
foreach (var c in result) {
Console.Write(" {0}", c);
}
}
}
So all good. Lets try and do the same thing in Java 8. So as not to get distracted by the details of how lambdas work we will write this as s -> null
and focus on simply invoking flatMap
.
The first thing we might naively try is:
data.flatMap(s -> null)
But an experienced Java developer will not because they have internalised that rule that arrays, whilst being the built in type, are not a built in type. If you parsed that preceding sentence without a cognitive flicker then congratulations on your knowledge of Java, but please go back and reread it as a beginner.
So what we need to do is convert the array to as list, perhaps using Arrays.asList. So our second attempt is:
Arrays.asList(data).flatMap(s -> null)
This brings us closer but still there is no method called flatMap . After reading a Java 8 tutorial we discover that one must call the stream method to access a stream of items against which the ‘functional toolkit’ can be run. So now we try:
Arrays.asList(data).stream().flatMap(s -> null);
And this compiles. Hurrah! Before we move on we can refactor a little by introducing a static import for asList:
import static java.util.Arrays.asList;
This will allow us to write:
asList(data).stream().flatMap(s -> null);
It looks like we are one simple step away from success. Our hearts swell with hope and our heads with dreams of coffee as we write:
asList(data).stream().flatMap(s -> s.toCharArray());
But this plunges us into the hell that is template expanded compiler errors:
We guess that the problem is one or both of these:
- We are returning an array
- We are returning primitives
- We can try returning a list:
asList(data).stream().flatMap(s -> asList(s.toCharArray()));
But this gives us an even more confusing message. In desperation we try returning a stream:
Stream results = asList(data).stream().flatMap(s -> asList(s.toCharArray()).stream());
This compiles and runs. But when we examine the output we have:
[C@448139f0 [C@7cca494b [C@7ba4f24f [C@3b9a45b3 [C@7699a589 [C@58372a00 [C@4dd8dc3
After caffeine, a walk around the block and maybe smashing up some furniture we realise what is happening. We have been bitten by the common mistake of assuming the return type of (for example) asList(int [] {12,13,14})
is a list of three Integer
objects, when in fact it is List<int[]>
Hence the lambda is returning a stream containing a single item which is an array of characters and flatMap is joining up the arrays themselves rather than the characters they contain.
So what we seem to need is specialised methods and/or stream classes that can cope with primitives. A quick hunt into the JDK Javadoc reveals that these things do exist:
However they only exist for integers and doubles. So all is fine unless your program needs to manipulate text for some reason.
There is of course nothing to prevent us writing our own conversion method:
public static Stream<Character> toStream(String input) {
Character[] output = new Character[input.length()];
for (int i = 0; i < input.length(); i++) {
output[i] = input.charAt(i);
}
return Arrays.stream(output);
}
Although functional purists will run screaming back to Haskell at the thought of writing an imperative method in order to make a core functional operation work. But it does work – heres the finished program:
import java.util.Arrays;
import static java.util.Arrays.asList;
import java.util.stream.Stream;
public class Program {
public static Stream toStream(String input) {
Character[] output = new Character[input.length()];
for (int i = 0; i < input.length(); i++) {
output[i] = input.charAt(i);
}
return Arrays.stream(output);
}
public static void main(String [] args) {
String [] data = {"super","cala","fragil","istic","expy","ali","dotious"};
Stream<? extends Character> results =
asList(data).stream().flatMap(s -> toStream(s));
for (Object obj : results.toArray()) {
System.out.printf("%s ",obj);
}
}
}
Note the need for the bounded type parameter on results and the fact the even with it toArray still returns an Object []. Also I could have used the method reference syntax Program::toStream
, which would have been less verbose had toStream been in a utility class. But we got there in the end.
This example may appear to be contrived to put Java 8 in a bad light. But it is actually the very first thing I tried and resulted in several hours of intrigue. When it comes to comparing Java 8 against other JVM languages the issue is not overall complexity but accidental vs. essential complexity. For example Scala is more complex than Java 8 in absolute terms but the complexity exists for a reason, that being to blend together the OO and FP styles. Once you understand the goals of the language the complexity starts to make sense.
Java 8 is now burdened with huge amounts of accidental complexity. The developer new to Java must cope with:
- The distinctions between arrays, lists and streams
- The distinction between value types and objects
- How wrapper types and boxing partially obscure the above
- The distinction between normal streams and streams of primitives
- The endless pit of complexity that is generics
Experienced Java developers will be able to shoulder this extra complexity and continue on. Like the lobster in the pot they have seen the heat gently increase over the last 15+ years and don’t mind the blisters. But spare a thought for future generations of graduates, with limited programming experience from college, who will be dropped fresh into the pot and experience all the pain at once…
A follow up to this post is available here