Java 8 lambdas
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:
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:
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:
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.
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:
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:
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:
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:
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.
References and further reading
The Java™ Tutorials: Lambda Expressions
The Java™ Language Specification for Java 8: 15.27 Lambda Expressions
The Java™ Language Specification for Java 8: 9.8 Functional Interfaces
JSR 335: Lambda Expressions for the Java™ Programming Language
Documentation for java.util.function
This package contains most of the standard functional interfaces in the JDK.
The code for this project is on GitHub
Step 0 is in master
, the other steps are in the other branches.