Lamda expressions were introduced in Java 8 and have been around for a while. They are in my opinion one of the better features of Java 8, allowing for a more functional approach to writing code, and thus enabling most of the java 8 features. So let’s take a closer look at lambda’s and see what they are, how to reason about them, and why they are a good addition.

Function signatures

Before we start with lambda’s let’s build op our way of thinking first using function signatures. A function signature is a way of writing down a very simple version of the function, describing only it’s input and it’s output. To write this we use the → notation, so it would be written as INPUT → OUTPUT. To give you an example, let’s look at the following function.

public int getLength(String s){
 return s.length();
}

The function signature in this case would be String → int (given a String, produce an int).

This also works for functions with multiple arguments:

public int getLength(String s, String s2){
 return s.length() + s2.length();
}

The function signature in this case would be (String, String) → int (given 2 Strings, produce an int).

It even works with Void

public void printName(Person p){
  System.out.println(p.getName());
}

The function signature in this case would be Person → Void (Given a person, do nothing).

public String getCompanyName(){
  return “JDriven”;
}

The function signature in this case would be Void → String (given no input, produce a String).

Function signatures provide a very abstract way of looking at a function, ignoring all implementation details (or even programming language) by only caring about input and output. Next time before you start coding on a problem, try to see if you can write the solution down using only Function signatures, and you will find that it helps a lot in coming up with a good solution.

Simplifying functions

With function signatures we simplify the function by removing everything apart from input and output. Another way to simplify a function is by only writing out the input and the body of the function.

Going back to our examples:

public int getLength(String s){
 return s.length();
}

The simplified version of this function would be (String s) → { return s.length(); }. Given a String s, return s.length()

The other examples could be written in a simplified way as well;

(String s1, String s2) → { return s1.length() + s2.length(); } //Given 2 strings, give back the combined length

(Person p) → { System.out.println(p.getName()); } //Given a Person, print out it’s name

() → { return “JDriven”; } //Given no input, return a String

So with a simplified function we only keep the bare minimum of coding required for a function, stating only it’s input and it’s body.

Functional interfaces

A functional interface is any interface that contains only 1 (non-default) method. There is also an annotation @FunctionalInterface that can be put on an interface to indicate it is a functional interface. It is not required to have this annotation for the interace to be considered a functional interface, but. However it is recommended to use this annotation since when used the compiler will check if it only has 1 function. An example of a functional interface would be:

@FunctionalInterface
public interface Lenghty{

  public int getLength(String s);
}

Because the annotation is not required and default methods don’t count the following would also be considered a functional interface

public interface Lenghty {

  public int getLength(String s);

  public default void printLength(String s) {
    System.out.println(s);
  }
}

Lambda’s

So, now we know what a function signature, a simplified function and a functional interface is, we can finally talk about lambda’s. A lambda is basically an implementation of a functional interface written as a simplified function. If we would implement the Lenghty interface, and put it in a variable it would look like this.

Lenghty myLambda = (String s) -> { return s.length(); };

We see our functional interface on our left, and our lambda function on the right side. We can call our lambda function like this

myLambda.getLength("JDriven");

Notice here the lambda itself has no function name (it’s just the input and the body), so the function name comes from the interface. Lets make it more interesting, and make a second functional interface

public interface Foo {

  public int bar(String s);
}

And now lets implement both functional interfaces with the same lambda.

Lenghty myLambda = (String s) -> { return s.length(); };
Foo myLambda2 = (String s) -> { return s.length(); };

myLambda.getLength("JDriven");
myLambda2.bar("JDriven");

So what is happening here; we have 2 interfaces and they can contain the same lambda! This is because the compiler only looks at the function signature, which is String → Int in this case. Both functional interfaces have the same function signature, so the same lambda applies for both. Notice that when calling the lambda, the method name does change based on which interface was implemented.

However this will not work

Lenghty myLambda3 = myLambda2;

Since we assigned types to the lamda’s, the compiler will not let us mix those up. So this makes it so that given the following function:

public void doFoo(Foo myLambda){
}

The following will not compile because we are supplying the wrong type

Lenghty myLambda = (String s) -> { return s.length(); };
doFoo(myLambda);

But these things will compile

Foo myFooLambda = (String s) -> { return s.length(); };
doFoo(myFooLambda);
doFoo((String s) -> { return s.length(); });

This is obvious for the first case, since we supply the correct type there. In the second case the compiler just checks the function signature of the supplied lambda to see if it matches the Foo type.

Syntax

So far we used the following lambda syntax: (String s) → { return s.length(); } But the java compiler is quite smart. First of all, if your function only has 1 line, we can remove the brackets and the return statement: (String s) → s.length(). And in most cases, the compiler can even figure out what the input variable type is. So we can even simplify it more: s → s.length() So all 3 of these are exactly the same lambda:

Lenghty myLambda = (String s) -> { return s.length(); };
Lenghty myLambda = (String s) -> s.length();
Lenghty myLambda = s -> s.length();

Basic lambda interfaces

As we just learned, a lambda only needs to follow the function signature of a functional interface. This means that we do not need to create a new functional interface every time we need one, we can re-use them (as long as they match the function signature). Because of this Java 8 comes with a few default functional interfaces which should handle most cases. Here are the most common used together with their function signature.

Function

Function<I, O>

The Function is one of the most used functional interfaces. This lambda has the function signature I → O. Given an I produce an O (e.g. Function<String, Integer>), given a String return an Integer) This is for example used in the List.stream().map() function, where the map() takes a Function as an argument to do the mapping operation

BiFunction

BiFunction<I, J, O>

This is basically the same as Function, but it accepts 2 arguments instead of only 1. This lambda has the function signature (I, J) → O. Given an I and a J, produce an O (e.g. Function<String, String, Integer>>, given 2 strings produce an integer)

Supplier

Supplier<O>

This is a lamda that can just produce something out of nowhere. It has the function signature Void → O. Given nothing, produce an O. This is quite useful in practice, since it allowes you to pass an argument to a function in a lazy way. (for example, the Optional.orElseThrow() has a supplier, meaning you need to pass () → new IllegalStateException() for example. Because a supplier is used, the IllegalStateException is not created until it is actually needed (if it is needed at all).

Consumer

Consumer<I>

This is the opposite of the Supplier. It only consumes something and returns nothing. This means it’s function signature is I → Void. This is for example used in the list.forEach(), where it just takes an element.

Predicate

Predicate<I>

The predicate is used to convert something to a boolean. It’s function signature is I → boolean. This is mainly used for filter operations like the List.stream().filter(). It takes an input object, and says if it passes the filter (true), or should be rejected (false)

Method references

Method references are basically an alternative lambda syntax. There are 3 kinds of method references: static, instance and constructor. But they basically all boil down to the same idea, they are just lambda’s.

Method reference example

Let’s look at the following lambda.

Lenghty myLambda = (String s) -> { return s.length(); };

It basically get’s a string, and calls the length() function on the string. It can also be described as Given a Type call this Function and we can use the :: notation for this

<Type>::<Function>

In this case it would be String::length. Where String is the input argument and length() the function that should be called. This method reference notation just returns a lambda that abides to this, which means that these two are equal:

Lenghty myLambda = (String s) -> { return s.length(); };
Lenghty myLambda = String::length

In these cases, I would suggest going for the method reference since it contains no code (unlike the lambda) and thus is less complex and cannot be implemented wrongly.

Why are lambda’s a good thing

With lambda’s we can store functions in variables and pass them along to other functions. This may not seem like a big deal, but it actually enables maximum re-usability. A lot of code is not reusable because at some point you need some context specific code. If we take the following piece of code for example.

List<String> names = List.of("Ties", "Cristina");
List<String> result = new ArrayList<>();
List<String> result2 = new ArrayList<>();
for(String name : names){
    result.add(name.toUpperCase());
}
for(String name : names){
    result2.add(name.toLowerCase());
}

Here we want to change an List to 2 Lists with either uppercase or lowercase strings. If we look at the code however we see that most of it is actually duplicate. The only thing that is different is what needs to happen to the input (either turn it to uppercase or turn it to lowercase). This is the case in a lot of code where the algorithm is exactly the same but there is a slight implementation detail that is different for each context. Let’s see how a lambda function can help with this.

List<String> names = List.of("Ties", "Cristina");
List<String> result = map(names, s -> s.toUpperCase()); //We can use a lambda
List<String> result2 = map(names, String::toLowerCase);	 //Or a method reference

public List<String> map(List<String> list, Function<String, String> myFunction){
    List<String> result = new ArrayList<>();
    for(String item : list){
        result.add(myFunction.apply(item));
    }
    return result;
}

If we extract the common part, we can now pass the one thing that is different based on the context as a function. This way we achieve maximum re-usability because we can now use our map function on any list of Strings to not only do uppercase and lowercase but to anything that has the function signature String → String. This is of course only a very simple simple example, but whole libraries have been build on this principle like the Java 8 streaming library. To learn why this is better than using a for loop, I suggest reading my blog about this

shadow-left