- 9 minutes read

Functional programming is en vogue. There are many good reasons to adopt functional programming. Recently, I watch more and more Java programmers using the new programming style. As things go, they experiment and play with their new toy, pushing it to the limits. This article briefly shows why Lambdas are useful, what Java programmers make of it, and concludes with a few recommendations to do it right.

Advantages of functional programming

Functional programming often allows writing programs in a few lines. There are many other advantages, but the concise programming style alone is enough to make functional programming attractive. Among other things, it gets rid of technical clutter like loop variables, allowing the reader to focus on what's really important.

To illustrate the point, let's have a look how to print the colors of a traffic light in JavaScript. If you're attuned to Java - don't worry, the code is simple, you'll be able to read it without problems.

let colors = ['red', 'yellow', 'green']; for (let index = 0; index < colors.length; index++) { console.log(colors[index]); }

The first line creates an array consisting of the three colors. The second line is boilerplate code. It introduces a loop variable and sees to it that the loop covers each element of the array.

That's good, but we aren't really interested in the loop variable. We are only interested in the array elements. Adding insult to injury, the second line also describes the precise order of the loop. In many cases, it's not important whether the loop starts with the last element, the first element or any other element. Functional programming allows us to get rid of this useless information:

colors.forEach(color => console.log(color));

Much better. We only have to decipher a single line. In theory, the loop can even run in parallel. Most modern CPUs have more than three cores, so each core could care about one of the array elements and print the colors in parallel (although JavaScript doesn't make use of this option).

Actually, parallelism was precisely the reason why Java adopted functional programming and streams. Probably that's the reason why so many programmers believe that streams are faster than traditional procedural programming.

In this particular case, the Java counterpart looks pretty nice:

List colors = Arrays.asList("red", "yellow", "green"); colors.forEach(color -> System.out.println(color));

Method handles

Java 8 also offers a fairly unique option to write even more concise code. Instead of implementing the Lambda properly, it suffices to pass a method handle. The Java compiler translates it back to the original Lambda expression:

List colors = Arrays.asList("red", "yellow", "green"); colors.forEach(System.out::println);

Many developers consider this easier to read.

Corner cases

The next example uses a method handle, too:

private int addNumbers(int uppperBound) { return IntStream.range(1, uppperBound) .reduce(Integer::sum) .getAsInt(); }

Now you have to be very familiar with the functional API of Java to understand what's going on. I'm not sure I like the reduce bit. The parameter is a method handle. How long does it take to see that it's simply the same as (a, b) -> a+b?

private int addNumbers(int uppperBound) { return IntStream.range(1, uppperBound) .reduce(0, (a, b) -> a+b); }

I, for one, consider the second version easier to read an maintain (although I'm still at odds with the map-reduce-paradigm. But that's probably simply a matter of getting used to it).

As a side effect, we got rid of the Optional. Method handles may be useful, but I suggest to use them rarely.

By the way, the Groovy approach is a lot more expressive, showing why I'm unhappy with the Java streams and the lack of syntactical sugar of Java:

private int addNumbers(int uppperBound) { // hint: "reduce" is called "inject" in Groovy (1..uppperBound).inject(0, (a, b) -> a+b); }

Goals of functional programming

Before continuing my rant, let's quickly summarize some advantages of functional programming I don't mention in this article.

  • Side-effect-free programming. Functional programming both teaches you and makes it easy to avoid side-effects. Quickly, you'll stop modifying global variables.
  • Functions are idempotent. That's very useful for unit tests.
  • Lambda expressions can be stored in variables. So you can pass them as parameters to functions, taking refactorings like "extract method" to another level.

Java weirdness

Obviously, functional programming is a very useful tool. Unfortunately, sometimes things get over the top. For instance, consider this example I've found at Mkyong.com:

List list = new ArrayList<>(); list.add(new Hosting(1, "liquidweb.com", 80000)); list.add(new Hosting(2, "linode.com", 90000)); list.add(new Hosting(3, "digitalocean.com", 120000)); list.add(new Hosting(4, "aws.amazon.com", 200000)); list.add(new Hosting(5, "mkyong.com", 1)); list.add(new Hosting(6, "linode.com", 100000)); Map result = list.stream() .sorted(Comparator.comparingLong(Hosting::getWebsites).reversed()) .collect( Collectors.toMap( Hosting::getName, Hosting::getWebsites, // key = name, value = websites (oldValue, newValue) -> oldValue, // if same key, take the old key LinkedHashMap::new // returns a LinkedHashMap, keep order ));

This may look like an extreme example, but it's something I see all the time. How long does it take to grasp what this code does?

Actually, it's very simple: it takes a list of providers, orders them by the number of websites they host, and converts the result as a LinkedHashMap.

In TypeScript, the same code can be expressed in merely two lines. There's no such thing as a LinkedHashMap in TypeScript, so I was a bit surprised that the final map even retains the reverse order.

const sorted = list.sort((a, b) => b.websites - a.websites); const result = new Map(sorted.map((site) => [site.provider, site])); result.forEach(site => console.log(${site.provider} ${site.websites});

Using Lambdas efficiently

Why is the TypeScript example so much shorter?

First of all, it uses some clever tricks of the language. So it demands a lot of the reader, too. You have to know about Json objects, and you have to know how the constructor of Map works. But that's pretty much common knowledge among TypeScript programmers, the same category as streams and Optionals in the Java world.

Plus, I omitted the type information for the sake of brevity. Actually, that's something I almost never do in real-world code. Type inference is a great tool, but I only use it if the type is absolutely clear from the context.

Back to the Lambda expressions (or closures, in TypeScript lingo).

The first hint is that the lambdas are short. Lambda expressions improve readability if they fit into a single line. Two or three lines may be ok, too, but you should think about extracting the Lambda body to a method.

The second difference is that TypeScript doesn't use method handles. Every Lambda expression describes explititely what happens. There's an input, and a rule what to do with the input.

Third, I recommend splitting long chains of operators into several statements, storing intermediate results in temporary variables. Giving the variables expressive names improves readability. Plus, debugging becomes easier that way.

Syntactical clutter of Java

Java adds a lot of syntactical clutter. With few exceptions, you have to convert your maps and lists into streams. Sometimes Java is just too clever, converting your type into an Optional, as we've seen above. So you have to unbox it again. Try to avoid Optionals unless you really need them. Like so often, it's a great idea adding a lot of power to your toolbox. Only sometimes it gets in your way when you don't need it.

Similarly, the end of stream operator often consists of weird expressions telling the JVM how to convert the stream back to a list or a hash map. Every other functional programming language I know doesn't need this, because it implements functional operators such as map, reduce, and filter directly on the collection types. If you map every element of a list, the result is a list. Filtering a map yields a map.

Wrapping it up

I've talked about this with many Java developers. Funny thing is, almost nobody sees the problem. After a while, they tell me, you just get used to it. You forget the clumsiness of these lines (found at Baeldung.com):

Map result = givenList.stream() .collect(toMap(Function.identity(), String::length))

Things like String::new and Function.identity() become natural to you after a while. You just have to learn them by heart and to accept them. Truth to tell, this syntactical clutter also offers a lot of flexibility. Every one in a while, it turns out to be an advantage over the streamlined API of other programming languages.

There's that. Java programmers are used to that kind of stuff. They've written thousands and thousands of getters and setters over the years, until the point they stop seeing them in the source code. All they see is a familiar pattern. Many even report it helps them. toMap(Function:identity(), String::length) belongs to the same category.

Obviously, it works for most Java programmers. Even so, I recommend learning a language using functional programming as a first-class citizen, such as Groovy or TypeScript. This teaches you what functional programming is meant to be. After that, you've probably acquired a more rational view on Java Lambdas. Don't use them because they are trendy. Don't use them because someone told you it's they way "we do it nowadays". Use them because they help you and your team. And remember: classical, procedural programming is still allowed, even in Java 8 and above. Usually, it's even faster. But that's another day's story.

Oh, and before I forget: check out the slides by Joshua Bloch on Effective Java. They inspired me to this article. Among other things, he shows that parallel execution is not always better than single-threaded execution.


Dig deeper

Is Java 8 a functional programming language?

Joshua Bloch on Effective Java


Comments