Chapter 1. Lambdas: Parameterizing Code by Behavior
Why Do I Need to Learn About Lambda Expressions?
Over the next two chapters, we’re going to be talking in depth about the relationship between functional and object-oriented programming principles, but first let’s cover some of the basics. We’re going to talk about a couple of the key language features that are related to functional programming: lambda expressions and method references.
Note
If you already have a background in functional programming, then you might want to skip this chapter and move along to the next one.
We’re also going to talk about the change in thinking that they enable which is key to functional thinking: parameterizing code by behavior. It’s this thinking in terms of functions and parameterizing by behavior rather than state which is key to differentiating functional programming from object-oriented programming. Theoretically this is something that we could have done in Java before with anonymous classes, but it was rarely done because they were so bulky and verbose.
We shall also be looking at the syntax of lambda expressions in the Java programming language. As I mentioned in the Introduction, a lot of these ideas go beyond Java; we are just using Java as a lingua-franca: a common language that many developers know well.
The Basics of Lambda Expressions
We will define a lambda expression as a concise way of describing an anonymous function. I appreciate that’s quite a lot to take in at once, so we’re going to explain what lambda expressions are by working through an example of some existing Java code. Swing is a platform-agnostic Java library for writing graphical user interfaces (GUIs). It has a fairly common idiom in which, in order to find out what your user did, you register an event listener. The event listener can then perform some action in response to the user input (see Example 1-1).
In this example, we’re creating a new object that provides an implementation of the ActionListener
class. This interface has a single method, actionPerformed
, which is called by the button
instance when a user actually clicks the on-screen button. The anonymous inner class provides the implementation of this method. In Example 1-1, all it does is print out a message to say that the button has been clicked.
Note
This is actually an example of behavior parameterization—we’re giving the button an object that represents an action.
Anonymous inner classes were designed to make it easier for Java programmers to represent and pass around behaviors. Unfortunately, they don’t make it easy enough. There are still four lines of boilerplate code required in order to call the single line of important logic.
Boilerplate isn’t the only issue, though: this code is fairly hard to read because it obscures the programmer’s intent. We don’t want to pass in an object; what we really want to do is pass in some behavior. In Java 8, we would write this code example as a lambda expression, as shown in Example 1-2.
Instead of passing in an object that implements an interface, we’re passing in a block of code—a function without a name. event
is the name of a parameter, the same parameter as in the anonymous inner class example. ->
separates the parameter from the body of the lambda expression, which is just some code that is run when a user clicks our button.
Another difference between this example and the anonymous inner class is how we declare the variable event
. Previously, we needed to explicitly provide its type—ActionEvent event
. In this example, we haven’t provided the type at all, yet this example still compiles. What is happening under the hood is that javac
is inferring the type of the variable event
from its context—here, from the signature of addActionListener
. What this means is that you don’t need to explicitly write out the type when it’s obvious. We’ll cover this inference in more detail soon, but first let’s take a look at the different ways we can write lambda expressions.
Note
Although lambda method parameters require less boilerplate code than was needed previously, they are still statically typed. For the sake of readability and familiarity, you have the option to include the type declarations, and sometimes the compiler just can’t work it out!
Method References
A common idiom you may have noticed is the creation of a lambda expression that calls a method on its parameter. If we want a lambda expression that gets the name of an artist, we would write the following:
artist -> artist.getName()
This is such a common idiom that there’s actually an abbreviated syntax for this that lets you reuse an existing method, called a method reference. If we were to write the previous lambda expression using a method reference, it would look like this:
Artist::getName
The standard form is Classname::methodName
. Remember that even though it’s a method, you don’t need to use brackets because you’re not actually calling the method. You’re providing the equivalent of a lambda expression that can be called in order to call the method. You can use method references in the same places as lambda expressions.
You can also call constructors using the same abbreviated syntax. If you were to use a lambda expression to create an Artist
, you might write:
(name, nationality) -> new Artist(name, nationality)
We can also write this using method references:
Artist::new
This code is not only shorter but also a lot easier to read. Artist::new
immediately tells you that you’re creating a new Artist
without your having to scan the whole line of code. Another thing to notice here is that method references automatically support multiple parameters, as long as you have the right functional interface.
It’s also possible to create arrays using this method. Here is how you would create a String
array:
String[]::new
When we were first exploring the Java 8 changes, a friend of mine said that method references “feel like cheating.” What he meant was that, having looked at how we can use lambda expressions to pass code around as if it were data, it felt like cheating to be able to reference a method directly.
In fact, method references are really making the concept of first-class functions explicit. This is the idea that we can pass behavior around and treat it like another value. For example, we can compose functions together.
Summary
Well, at one level we’ve learnt a little bit of new syntax that has been introduced in Java 8, which reduces boilerplate for callbacks and event handlers. But actually there’s a bigger picture to these changes. We can now reduce the boilerplate around passing blocks of behavior: we’re treating functions as first-class citizens. This makes parameterizing code by behavior a lot more attractive. This is key to functional programming, so key in fact that it has an associated name: higher-order functions.
Higher-order functions are just functions, methods, that return other functions or take functions as a parameter. In the next chapter we’ll see that a lot of design principles in object-oriented programming can be simplified by the adoption of functional concepts like higher-order functions. Then we’ll look at how many of the behavioral design patterns are actually doing a similar job to higher-order functions.
Get Object-Oriented vs. Functional Programming now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.