Java 8 lambdas

Gillis J. de Nijs
8 min readNov 14, 2021

--

At my current job, I’m programming Java, mostly. Every once in a while, I train people for their Java certificates.

Some of the most common questions I get — both from new developers and more seasoned ones — are about lambdas: “What are they? How do they work? How should we think about them?”

And one of the common explanations is: “It’s new (well, it was, when Java 8 was introduced), and you just have to learn it and its syntax.”

I disagree. It’s new to Java 8 obviously, but it can be explained in terms you might already know and be more familiar with.

Step 0: The initial program

So, let’s begin with an example:

Step 0: Our initial program.

This should look familiar to you; it’s a regular Java program. This one has an Animal interface, with three concrete implementations (Fish, Frog, and Kangaroo), there’s an AnimalMatcher interface, and there’s a Main class that ties it all together. Notice that the Main class contains two inner classes that are implementations of the AnimalMatcher interface.

When we run this program, the output is as expected:

Frog can hop!
Frog can swim!
Kangaroo can hop!
Fish can swim!

Step 1: Removing the inner classes

Now we’ve seen this works, let’s start by changing the inner classes to anonymous classes, like so:

Step 1: The inner classes are now gone, and have been changed to anonymous classes.

You can see the change highlighted here. The program hasn’t changed functionally, as we can see from the output, which I won’t repeat here.

Step 2: Our first lambda!

Ok, now it gets interesting. Moving from an anonymous class to a lambda is really straightforward, as you can see from this overly whitespaced version:

Step 2: Our first lambda!

You can spot the change even better, if you compare the two files. So, what changed? I think there are three main parts to it:

  • Add an arrow -> between the method signature and the method body;
  • Remove the constructor call;
  • Remove the method name and return type, but not the parameters.

What you end up with, is a lambda. So, how does this work?

  • The arrow denotes that it’s a lambda. This is one way to spot them! It separates the parameters from the body.
  • We don’t need the constructor call, since we already know what we’re implementing. If you look at the assignment on line 9, you can easily spot we’re assigning this lambda to an AnimalMatcher. Java knows this too, so there’s no point in specifying that (again). You can compare this to the diamond operator in generics, where you don’t need to repeat the generic type.
  • You also don’t need to specify the method name, since there is only one abstract method to be implemented. (More on this at the end.) We do need the parameters though, since we need a way to access them in the method/lambda body.

Sweet! Still with me? Good. If not, take a bit of time to read this again, and try it out in your favorite IDE.

Step 3: Remove whitespace

This step is easy, it’s just getting rid of all unnecessary whitespace. It’s starting to look more like a lambda, if you’ve come across them before.

Step 3: Remove unnecessary whitespace.

It’s the exact same thing as before. We didn’t change any code, just removed whitespace. I told you it was an easy step…! 😀

Step 4: Expression lambda

We’re doing a lot better in terms of useless code. We removed the class name and method name, and the entire implementation is now on one line. It’s still perfectly readable too! (You should strive for readable code; the people that come after you will love you for it.)

But we can do better. When you have a lambda body that consists of a single statement, you can get rid of the curly braces. This is similar to if/while/for statements, where you can leave the braces off if you have just a single statement to execute. With lambdas however, this also means you have to remove the return. This is called an expression lambda:

Step 4: Change the verbose lambda to an expression lambda.

Looks neat, right? We should probably call it a day, except…

Step 5: Remove parameter type

Shorter still, you ask? Yes! And there are even more steps! But those are easy and are mostly about cleaning up the code.

Anyway, since we could get rid of the method name, why not get rid of the parameter type too? We already know which method we’re implementing, so we don’t need to make the parameter type explicit. It’s easy:

Step 5: Remove the explicit parameter type in the lambda definition.

Also note that we removed the parentheses around the parameter; we don’t need them here. I like this step. It’s still very easy to read and it immediately conveys what the lambda does. It takes an Animal and returns the result of the method call (a boolean in this case).

The name of the parameter is animal, and I would advice you to use something similar. You can obviously call it anything you like — since it’s just the placeholder for the parameter — but if you give it a useful name, you don’t have to look for the implementation. It takes away the guesswork. “Oh, right, this is an Animal.”

Step 6: Method reference

As you might have noticed, we’re now at a point where the method returns a boolean, and gets that information directly from the Animal we pass as a parameter. So, wouldn’t it be nice if we could use a shorthand notation for “a method on Animal that returns a boolean to match the AnimalMatcher interface”? It turns out, we can. We call this a method reference, and it looks like this:

This is as short as it can get. Animal::canHop references the boolean canHop(); method in Animal. It’s a perfect match for boolean matches(Animal animal); in AnimalMatcher. For those interested, there’s an extra paragraph below with more information.

Step 7: Using standard interfaces

Obviously, a method that acts on an object and returns a boolean is so common, that it would be weird if Java didn’t support that out-of-the-box. Fortunately, it does. Java 8 introduces the Predicate<T> interface that does exactly that. It has one abstract method: boolean test(T t);. We can easily refactor our program to use that interface. We’ll do it in two steps:

Step 7: Extend the Predicate interface.

In this step, we first change the AnimalMatcher interface to extend Predicate<Animal>. Since Predicate uses a different name for the abstract method, we have to change that too. Both changes are easily visible when you compare the files.

Step 8: Cleaning up

Now that we’re using the Predicate, our AnimalMatcher interface doesn’t add anything anymore, and we can remove it:

Step 8: Cleaning up.

The AnimalMatcher interface has been removed, and we now use Predicate<Animal> in our Main class. The change is visible here. This is the best we can do for now.

Wrapping up

We’ve seen how we can transform a simple program using lambdas. We started out with inner classes and a customer interface, and ended up with method references and a standard interface. If you came this far, well done!

Obviously, this isn’t the entire story. Lambdas can do so much more. I tend to use Predicate<T> as an example because it’s really clear what happens and how. The longer version is that you can use lambdas almost anywhere where you wish to implement an interface with exactly one abstract method. The arguments and return type of that method don’t really matter. You can go from no arguments, no body, all the way up to many arguments and a long body. If your body gets too long though, you should probably consider refactoring your code, because in my opinion, readability is key.

I put some extras below if you want to dive deeper.

More lambdas

In this case, we’ve seen an implementation for boolean matches(Animal animal) using a method reference Animal::canHop. This works, because canHop() in Animal returns a boolean.

But lambdas also work for other return types and parameters. If you have a method X getX(); on class Y for example, you can reference that using Y::getX. You could use this method reference to implement X something(Y y); for example.

And that’s not all. You can write lambdas that have a void return type, and lambdas that have zero or more than one argument. There’s a list of possibilities in §15.27 of the Java™ Language Specification:

() -> {}                // No parameters; result is void
() -> 42 // No parameters, expression body
() -> null // No parameters, expression body
() -> { return 42; } // No parameters, block body with return
() -> { System.gc(); } // No parameters, void block body

() -> { // Complex block body with returns
if (true) return 12;
else {
int result = 15;
for (int i = 1; i < 10; i++)
result *= i;
return result;
}
}

(int x) -> x+1 // Single declared-type parameter
(int x) -> { return x+1; } // Single declared-type parameter
(x) -> x+1 // Single inferred-type parameter
x -> x+1 // Parentheses optional for
// single inferred-type parameter

(String s) -> s.length() // Single declared-type parameter
(Thread t) -> { t.start(); } // Single declared-type parameter
s -> s.length() // Single inferred-type parameter
t -> { t.start(); } // Single inferred-type parameter

(int x, int y) -> x+y // Multiple declared-type parameters
(x, y) -> x+y // Multiple inferred-type parameters
(x, int y) -> x+y // Illegal: can't mix inferred and declared types
(x, final y) -> x+y // Illegal: no modifiers with inferred types

@FunctionalInterface

The sharp-eyed readers among you may have noticed a new annotation in Step 0. The AnimalMatcher has been annotated as being a @FunctionalInterface. But what is that?

The annotation acts as a safeguard for the developer when compiling your code, and as a note to your fellow developers that read the code. When this annotation is present on an interface, the compiler will check that the interface has exactly one abstract method. Not zero, not two, not more; exactly one. Because interfaces with exactly one abstract method are perfect (i.e., the only) candidates for lambdas.

If an interface has exactly one method to implement, we don’t have to specify the name of the method when we implement it using the lambda. After all, there is only one abstract method, so the lambda must refer to that one method. If we have more than one abstract method, the compiler can’t tell which method you mean.

Adding the safeguard is therefore a very good idea. If you don’t have exactly one abstract method, your lambda will fail to compile. This annotation on the interface takes care of two things:

  • It tells the compiler to fail compiling that interface if the condition isn’t met;
  • It tells your colleagues that they shouldn’t modify the interface by adding abstract methods, since that will break the lambdas.

Keep in mind though, that this annotation only counts abstract methods. It’s perfectly fine to have static or default methods in the interface, alongside your one abstract method.

--

--