Best Practices in Java 8


  • 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.
  • For 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()

Read More:

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.

public interface Debuggable {
default String debug() {
StringBuilder sb = new StringBuilder(this.getClass().getName());
sb.append(” [ “);
Field[] fields = this.getClass().getDeclaredFields();
for(Field f: fields) {
try {
sb.append(f.getName() + ” = ” + f.get(this));
sb.append(“, “);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new RuntimeException(e);
return sb.toString();

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:

public class Main implements Debuggable {
int a = 100;
String b = “Home”;
public static void main(String[] args) {
Main m = new Main();

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.

// takes a Long, returns a String
Function<Long, String> f = l -> l.toString();
// takes nothing gives you Threads
Supplier<Thread> s =Thread::currentThread;
// takes a string as the parameter
Consumer<String> c = System.out::println;

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:

// use them in with streams
new ArrayList<String>().stream().
// peek: debug streams without changes
peek(e -> System.out.println(e)).
// map: convert every element into something
map(e -> e.hashCode()).
// filter: pass some elements through
filter(e -> ((e.hashCode() % 2) == 0)).
// collect the stream into a collection

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.

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.
  • 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.

User user = getUser(name); // might return null
Location location = null;
if(user != null) {
location = getLocation(user);


Optional<User> user = Optional.ofNullable(getUser(user));
Optional<Location> location = -> getLocation(user));

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:

public <U> Optional<U> flatMap(Function<? super T,Optional<U>> mapper)

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!