A Gentle Introduction to Lambda Expressions in Java

Jameson Williams
3 min readJan 8, 2020
// 😨
downloadButton.setOnClickListener(() ->
api.get(CAT_PIC_URL,
bitmap -> catPicImageView.setBitmap(bitmap),
error -> showToast("Check your network connection.")
)
);

I was already a competent Java programmer when I first encountered a lambda expression. Still, I found them pretty mystifying. A lot of the documentation speaks of them in abstractions, and assuming you know some functional programming concepts.

This is an attempt to provide a gentle introduction to lambda expressions. What are they? How and why would you use them?

Runnable

An easy entry point is the Runnable. You can post some code to execute on a Handler:

Handler handler = new Handler(Looper.getMainLooper());
Runnable runnable = new Runnable() {
@Override
public void run() {
Log.i("Demo", "It ran!");
}
};
handler.post(runnable);

Here, we have created a Handler which is able to accept runnable code blocks. We pass an anonymous implementation of the Runnable interface.

The Runnable interface is an example of a “simple functional” interface, in that it is basically just some boiler plate around a single method: run() . This is the core requirement for something to be expressible as a lambda. You have to be implementing an interface, and the interface has to be a simple functional one.

We can use short-hand syntax for the new Runnable() {} and its single (inferred) run() method, on the right-hand-side of our assignment:

Handler handler = new Handler(Looper.getMainLooper());
Runnable runnable = () -> {
Log.i("Demo", "It ran!");
};
handler.post(runnable);

The () expressed the empty arguments of the run() method. The -> is short hand for “do the stuff that follows this.”

Since Log.i("Demo", "It ran!") is a single line, we can also skip the curly braces in this case. This is very much the same as writing if (true) { doFoo(); } vs. if (true) doFoo();

Handler handler = new Handler(Looper.getMainLooper());
Runnable runnable = () -> Log.i("Demo", "It ran!");
handler.post(runnable);

At this point, we don’t really need to cache the lambda into the runnable variable. We could just pass it directly to the post(Runnable runnable) method:

new Handler(Looper.getMainLooper())
.post(() -> Log.i("Demo", "It ran!"));

So, we’ve gone from 8 lines to 2. Pretty cool.

Parameters and Return Values

Runnable is simple example. But what if we want to use something more complex? Let’s make our own “simple functional interface”, to illustrate.

interface BinaryOperation {
int apply(int left, int right);
}

Above, we have defined a Java interface that defines an BinaryOperation . However we achieve apply , the operation must accept two int parameters and return only one.

An obvious example is addition. We could write an implementation of this interface:

class AdditionOperation implements BinaryOperation {
@Override
public int apply(int left, int right) {
return left + right;
}
}
BinaryOperation operation = new AdditionOperation();
System.out.println("result = " + operation.apply(1, 2));
// Prints 3.

But, that’s kind of a lot. Isn’t it?

Using our same formula from Runnable , let’s implement it with a lambda expression.

BinaryOperation operation = (left, right) -> left + right;
System.out.println("result = " + operation.apply(1, 2));
// Prints 3, too.

As earlier, we have (left, right) which is short-hand for the (int left, int right) argument labels on the int apply(int left, int right) method we defined.

The type of the interface, BinaryOperation , is inferred from the type value of the operation variable.

The name of the single method in that interface, apply , is discarded. -> says “when, then do”, as before.

Lastly, we have a single-statement body for the lambda expression, left + right , which is equivalent to the contents of our fully-dressed class implementation, above.

“Mental Reverse-Engineering”

Now that we know a little bit about what syntax is discarded when we use lambdas, we can go back and mentally reconstruct the code at the beginning of this article.

downloadButton.setOnClickListener(view ->
api.downloadBitmap(
CAT_PIC_URL,
bitmap -> catPicImageView.setBitmap(bitmap),
error -> showToast("Check your network connection.")
)
);

When-ever a button gets clicked, a lambda expression is called. That expression responds to the button click by trying to download a bitmap image.

The method that downloads a bitmap has two outcomes. Either it returns a bitmap, or it returns an error explaining why it couldn’t obtain a bitmap.

When the bitmap callback occurs, the code sets the bitmap to be displayed in a UI view. If the bitmap download fails, the error callback will display a message to the user, suggesting to check their internet connection.

We can imagine that this is actually expressing something in fully-dressed syntax, like:

downloadButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
api.downloadBitmap(
CAT_PIC_URL,
new OnBitmapAvailableCallback() {
@Override
public void onBitmapAvailable(Bitmap bitmap) {
catPicImageView.setBitmap(bitmap);
}
},
new OnDownloadFailedCallback() {
@Override
public void onDownloadFailed(Throwable error) {
showToast("Check your internet connection.");
}
}
);
}
});

--

--