The term lambda, which has its roots in lambda calculus, developed by Alonzo Church in 1936, simply refers to an anonymous function. Typically, a function (or method, in more proper Java parlance), is a statically-named artifact in the Java source:
public int add(int x, int y) {
return x + y;
}
This simple method is one named add that takes two int parameters as well as returning an int parameter. With the introduction of lambdas, this can now be written as follows:
(int x, int y) → x + y
Or, more simply as this:
(x, y) → x + y
This abbreviated syntax indicates that we have a function that takes two parameters and returns their sum. Depending on where this lambda is used, the types of the parameters can be inferred by the compiler, making the second, even more concise format possible. Most importantly, though, note that this method is no longer named. Unless it is assigned to a variable or passed as a parameter (more on this later), it can not be referenced--or used--anywhere in the system.
This example, of course, is absurdly simple. A better example of this might be in one of the many APIs where the method's parameter is an implementation of what is known as a Single Abstract Method (SAM) interface, which is, at least until Java 8, an interface with a single method. One of the canonical examples of a SAM is Runnable. Here is an example of the pre-lambda Runnable usage:
Runnable r = new Runnable() {
public void run() {
System.out.println("Do some work");
}
};
Thread t = new Thread(r);
t.start();
With Java 8 lambdas, this code can be vastly simplified to this:
Thread t = new Thread(() ->
System.out.println("Do some work"));
t.start();
The body of the Runnable method is still pretty trivial, but the gains in clarity and conciseness should be pretty obvious.
While lambdas are anonymous functions (that is, they have no names), Java lambdas, as is the case in many other languages, can also be assigned to variables and passed as parameters (indeed, the functionality would be almost worthless without this capability). Revisiting the Runnable method in the preceding code, we can separate the declaration and the use of Runnable as follows:
Runnable r = () {
// Acquire database connection
// Do something really expensive
};
Thread t = new Thread(r);
t.start();
This is intentionally more verbose than the preceding example. The stubbed out body of the Runnable method is intended to mimic, after a fashion, how a real-world Runnable may look and why one may want to assign the newly-defined Runnable method to a variable in spite of the conciseness that lambdas offer. This new lambda syntax allows us to declare the body of the Runnable method without having to worry about method names, signatures, and so on. It is true that any decent IDE would help with this kind of boilerplate, but this new syntax gives you, and the countless developers who will maintain your code, much less noise to have to parse when debugging the code.
Any SAM interface can be written as a lambda. Do you have a comparator that you really only need to use once?
List<Student> students = getStudents();
students.sort((one, two) -> one.getGrade() - two.getGrade());
How about ActionListener?
saveButton.setOnAction((event) -> saveAndClose());
Additionally, you can use your own SAM interfaces in lambdas as follows:
public <T> interface Validator<T> {
boolean isValid(T value);
}
cardProcessor.setValidator((card)
card.getNumber().startsWith("1234"));
One of the advantages of this approach is that it not only makes the consuming code more concise, but it also reduces the level of effort, such as it is, in creating some of these concrete SAM instances. That is to say, rather than having to decide between an anonymous class and a concrete, named class, the developer can declare it inline, cleanly and concisely.
In addition to the SAMs Java developers have been using for years, Java 8 introduced a number of functional interfaces to help facilitate more functional style programming. The Java 8 Javadoc lists 43 different interfaces. Of these, there are a handful of basic function shapes that you should know of, some of which are as follows:
BiConsumer<T,U>
|
This represents an operation that accepts two input arguments and returns no result
|
BiFunction<T,U,R>
|
This represents a function that accepts two arguments and produces a result
|
BinaryOperator<T>
|
This represents an operation upon two operands of the same type, producing a result of the same type as the operands
|
BiPredicate<T,U>
|
This represents a predicate (Boolean-valued function) of two arguments
|
Consumer<T>
|
This represents an operation that accepts a single input argument and returns no result
|
Function<T,R>
|
This represents a function that accepts one argument and produces a result
|
Predicate<T>
|
This represents a predicate (Boolean-valued function) of one argument
|
Supplier<T>
|
This represents a supplier of results
|
There are a myriad of uses for these interfaces, but perhaps the best way to demonstrate some of them is to turn our attention to the next big feature in Java 8--Streams.