Best practices when you use Lambda expressions in Java
Overview
Now that Java 8 has reached wide usage, patterns, and best practices have begun to emerge for some of its headlining features. In this post, I will take a closer look to functional interfaces and lambda expressions. A lambda expression is a block of code that can be passed around to execute. It is a common feature for some programming languages, such as Lisp, Python, Scala, etc. But before Java 8, we can not do the same in Java. From Java 8, lambda expressions enable us to treat functionality as method argument and pass a block of code around. Lambda expressions in Java 8 are powerful and therefore very compelling. When you finish to read it post you’ll acquire the best practices to work with Lambda expressions, you gonna rock!!!
Prefer Standard Functional Interfaces
Functional interfaces, which are gathered in the java.util.function package, satisfy most developers’ needs in providing target types for lambda expressions and method references. Each of these interfaces is general and abstract, making them easy to adapt to almost any lambda expression. Developers should explore this package before creating new functional interfaces.
Consider an interface Todo:
@FunctionalInterfacepublic interface Todo {String method(String string);}
and a method add() in some class UseTodo, which takes this interface as a parameter:
public String add(String string, Todo todo) {return todo.method(string);}
To execute it, you would write:
Todo todo = parameter -> parameter + " from lambda";String result = useTodo.add("Message ", todo);
Look closer and you will see that Todo is nothing more than a function that accepts one argument and produces a result. Java 8 already provides such an interface in Function<T,R> from the java.util.functionpackage.
Now we can remove interface Todo completely and change our code to:
public String add(String string, Function<String, String> fn) {return fn.apply(string);
}
To execute this, we can write:
Function<String, String> fn =parameter -> parameter + " from lambda";String result = useTodo.add("I've added something", fn);
Don’t Overuse Default Methods in Functional Interfaces
You can easily add default methods to the functional interface. This is acceptable to the functional interface contract as long as there is only one abstract method declaration:
@FunctionalInterfacepublic interface Foo {String method();default void defaultMethod() {}}
Functional interfaces can be extended by other functional interfaces if their abstract methods have the same signature. For example:
@FunctionalInterfacepublic interface FooExtended extends Baz, Bar {}@FunctionalInterfacepublic interface Baz {String method();default void defaultBaz() {}}@FunctionalInterfacepublic interface Bar {String method();default void defaultBar() {}}
Just as with regular interfaces, extending different functional interfaces with the same default method can be problematic. For example, assume that interfaces Bar and Baz both have a default method defaultCommon(). In this case, you will get a compile-time error:
interface Foo inherits unrelated defaults for defaultCommon() from types Baz and Bar...
To fix this, defaultCommon() method should be overridden in the Foo interface. You can, of course, provide a custom implementation of this method. But if you want to use one of the parent interfaces’ implementations (for example, from the Baz interface), add following line of code to the defaultCommon() method’s body:
Baz.super.defaultCommon();
But be careful. Adding too many default methods to the interface is not a very good architectural decision. It is should be viewed as a compromise, only to be used when required, for upgrading existing interfaces without breaking backward compatibility.
Avoid Overloading Methods with Functional Interfaces as Parameters
Use methods with different names to avoid collisions; let’s look at an example:
public interface Adder {String add(Function<String, String> f);void add(Consumer<Integer> f);}public class AdderImpl implements Adder {@Overridepublic String add(Function<String, String> f) {return f.apply("Something ");}@Overridepublic void add(Consumer<Integer> f) {}}
At first glance, this seems reasonable. But any attempt to execute any of AdderImpl’s methods:
String r = adderImpl.add(a -> a + " from lambda");
ends with an error with the following message:
reference to add is ambiguous both methodadd(java.util.function.Function<java.lang.String,java.lang.String>)in fiandlambdas.AdderImpl and methodadd(java.util.function.Consumer<java.lang.Integer>)in fiandlambdas.AdderImpl match
To solve this problem, you have two options. The first is to use methods with different names:
String addWithFunction(Function<String, String> f);void addWithConsumer(Consumer<Integer> f);
The second is to perform casting manually. This is not preferred.
String r = Adder.add((Function) a -> a + " from lambda");
Use Method References
Very often, even in our previous examples, lambda expressions just call methods which are already implemented elsewhere. In this situation, it is very useful to use another Java 8 feature: method references.
So, the lambda expression:
a -> a.toLowerCase();
could be substituted by:
String::toLowerCase;
This is not always shorter, but it makes the code more readable.
Avoid Return Statement and Braces
Braces and return statements are optional in one-line lambda bodies. This means, that they can be omitted for clarity and conciseness.
Do this:
a -> a.toLowerCase();
instead of this:
a -> {return
a.toLowerCase()};
Avoid Parentheses Around a Single Parameter
Lambda syntax requires parentheses only around more than one parameter or when there is no parameter at all. That is why it is safe to make your code a little bit shorter and to exclude parentheses when there is only one parameter.
So, do this:
a -> a.toLowerCase();
instead of this:
(a) -> a.toLowerCase();
Avoid Specifying Parameter Types
A compiler in most cases is able to resolve the type of lambda parameters with the help of type inference. Therefore, adding a type to the parameters is optional and can be omitted.
Do this:
(a, b) -> a.toLowerCase() + b.toLowerCase();
instead of this:
(String
a, String
b) -> a.toLowerCase() + b.toLowerCase();
Avoid Blocks of Code in Lambda’s Body
In an ideal situation, lambdas should be written in one line of code. With this approach, the lambda is a self-explanatory construction, which declares what action should be executed with what data (in the case of lambdas with parameters).
If you have a large block of code, the lambda’s functionality is not immediately clear.
With this in mind, do the following:
Foo foo = parameter -> buildString(parameter);
private String buildString(String parameter) {String result = "Something " + parameter;//many lines of codereturn result;}
instead of:
Foo foo = parameter -> { String result = "Something " + parameter;//many lines of codereturn result;};
However, please don’t use this “one-line lambda” rule as dogma. If you have two or three lines in lambda’s definition, it may not be valuable to extract that code into another method.
Conclusion
In this post, we saw some best practices and pitfalls in Java 8’s lambda expressions and functional interfaces. Despite the utility and power of these new features, they are just tools. Every developer should pay attention while using them.
If possible, use one line constructions instead of a large block of code. Remember lambdas should be anexpression, not a narrative. Despite its concise syntax, lambdas should precisely express the functionality they provide.
This is mainly stylistic advice, as performance will not change drastically. In general, however, it is much easier to understand and to work with such code.