December 9, 2015

In this post we’ll look at the hot topics of the Java 8 language: default methods, lambdas and streams, and using Optional to represent absent values. In addition, we'll talk about the Java 8 best practices that we've found so far. If you're just here for our Java 8 cheat sheet, you can click the image or button at the bottom of the page to download a printable PDF version.

Best Practices in Java 8

Now that we've had a while to get to know JDK 8, we have a handful of Java best practices for the methods, Lambdas and the java.util.Optional container. At a glance, the best practices we've outlined in our cheat sheet are:

For Default methods - use 1 default method per interface, and don't enhance functional interfaces. Instead, you'll focus on conservative implementations for those enhancements.

use 1 default method per interface, and don't enhance functional interfaces. Instead, you'll focus on conservative implementations for those enhancements. For Lambdas - use expressions instead of statements, refactoring to use method references and chaining Lambdas.

use expressions instead of statements, refactoring to use method references and chaining Lambdas. For java.util.Optional - use plain objects within fields and method parameters and optional for return values. Then, instead of get(), you'll want to use orElse()

Want more detail? Check out the sections below.

Using Default Methods in Interfaces

The ability to specify default method implementations in interfaces was added into JDK 8 so that collections could evolve without breaking backward compatibility. Previously, we couldn’t just add a method to an interface without requiring all the implementing subclasses to specify an implementation of the new method. Breaking backward compatibility is a deal-breaker for Java. So since version 1.8 we can mark a method with the default keyword and provide the body of the method right in the interface. This, as any powerful feature does, opens all sorts of doors for un-maintainable, confusing code, if abused. However in small doses one can enhance existing interfaces and make them more useful in the codebase.

The main rule of thumb for using default methods is not to abuse them and not to make the code messier than it would be without it. For example, if you want to add some sort of functionality to Java classes without polluting their hierarchy with a common superclass, consider creating a separate interface just for this one utility method. Here’s an example of an interface called Debuggable that uses the reflection API to get the access to the object’s fields and provides a decent toString() implementation for the object that prints the fields values.

The important part here is the default keyword on the method signature. One could now use this for any class that you need to peek at, like this:

Which prints the expected line: “ Main [ a = 100 b = Home ] ”.

Functional interfaces of Java 8 deserve a special mention here. A functional interface is one that declares a single abstract method. This method will be called if we use it with the lambda syntax later. Note that default methods don’t break single abstract method contract. You can have a functional interface bearing many default methods if you choose. Don’t overuse it though. Code conciseness is important, but code readability trumps it by far.

Using Lambdas in Streams

Lambdas, oh sweet lambdas! Java developers have been eagerly waiting for you. For years Java has received the label of not being an appropriate programming language for functional programming techniques, because functions were not the first class citizens in the language. Indeed, there wasn’t a neat and accepted way to refer to a code block by a name and pass it around. Lambdas in JDK 8 changed that. Now we can use method references, for better or worse, to refer to a specific method, assign the functions into variables, pass them around, compose them and enjoy all the perks the functional programming paradigm offers.

The basics are simple enough, you can define functions using the arrow (->) notation and assign them to fields. To make things simpler when passing functions as parameters, we can use a functional interface, with only one abstract method.

There are a bunch of interfaces in the JDK that are created for almost any case: void functions, no parameters functions, normal functions that have both parameters and the return values. Here’s a taste of how your code might look using the lambda syntax.

The caveat here is that the code is tough to manage if you let your anonymous functions grow over a certain threshold. Think about the fattest lambda you’ve seen? Right. That should have never existed.

The most natural place for a lambda to exist is code which processes data. The code specifies the data flow and you just plug in the specific functionality that you want to run into the framework. The Stream API immediately comes to mind. Here’s an example:

That’s pretty self-explanatory, right?

In general, when working with streams, you transform the values contained in the stream with the functions you provide for example using the lambda syntax. However, for a better explanation, check out the Java 8 Streams cheat sheet, it has a short, clear explanation when and why you want to use certain stream methods and what pitfalls might await you.

Lambda Takeaways

If the code doesn’t specify the framework for the data flow into which you plug your functions, consider avoiding multiplying lambdas. A proper class might be more readable.

If your lambda grows above 3 lines of code - split it: either into several map() invocations that process the data in steps or extract a method and use the method reference syntax to refer to it.

invocations that process the data in steps or extract a method and use the method reference syntax to refer to it. Don’t assign lambdas and functions to the fields of the objects. Lambdas represent functions and those are best served pure.

Using java.util.Optional

Optional is a new type in Java 8 that wraps either a value or null, to represent the absence of a value. The main benefit is that it your code can now gracefully handle the existence of null values, as you don’t have to explicitly check for nulls anymore.

Optional is a monadic type you can map functions into, that will transform the value inside the Optional. Here’s a simple example, imagine you have an API call that might return a value or null, which you need to then process with the transform() method call. We’ll compare code with and without using Optional types.

vs.

It isn’t nice to have null checks polluting your code, is it? The best part is that we can now live inside this Optional world and never leave it, since all functions can be mapped into it. What about the functions that already return an Optional? Have no fear with the flatMap method, you won’t end up double wrapping Optionals. Check out flatMap’s signature:

It takes care of the required unwrapping so you have just one level of Optionals!

Using Optional Types

Now, before you rewrite all your code to have Optionals all over the place. Hold on for a minute longer. Here’s a rule of thumb for where you want to use Optional types:

Instance fields - use plain values. Optional wasn’t created for usage in fields. It’s not serializable and adds a wrapping overhead that you don’t need. Instead use them on the method when you process the data from the fields.

Method parameters - use plain values. Dealing with tons of Optionals pollutes the method signatures and makes the code harder to read and maintain. All functions can be lifted to operate on optional types where needed. Keep the code simple!

Method return values - consider using Optional. Instead of returning nulls, the Optional type might be better. Whoever uses the code will be forced to handle the null case and it makes for cleaner code.

All in all, using the Optional type correctly helps you keep your codebase clean and readable. And that’s very important! Disregarding that, both in life and in your code, is a recipe for disaster!

Download the Java 8 Cheat Sheet

I hope this post gives you an idea of some of the best practices on using Java 8 features and still have readable and maintainable code. If you liked it, don’t forget to print the handy 1 page cheat sheet we prepared with the takeaways from the post so you can print it out and put it under your less experienced colleague’s coffee cup. Or, if you're looking for our full collection of Java cheat sheets, be sure to visit our Java resources hub.

Download the Cheat Sheet

What are your rules of thumb and best practices with dealing with Java 8 features? Hit the comment section below or find us on Twitter: @JRebel_Java.