Module 3: Java Lambda Expressions
Learning Objectives
- Determine which Java functional interface is implemented by a given lambda expression
- Design and implement a lambda expression that implements the Supplier interface
- Design and implement a lambda expression that implements the Consumer interface
- Design and implement a lambda expression that implements the Predicate interface
- Design and implement a lambda expression that implements the Function interface
- Use a method reference to specify a specific object's instance method when a method accepts a functional interface argument
- Use a method reference to specify a class's static method when a method accepts a functional interface argument
- Explain that a lambda expression can be used to implement a functional interface
- Explain what an anonymous method is
- Define the method signature for a Function's abstract method
- Define the method signature for a Suppliers's abstract method
- Define the method signature for a Consumer's abstract method
- Define the method signature for a Predicate's abstract method
- Outline how to handle checked exceptions thrown inside functional interface implementations
- Use a method reference to specify an instance method callable on any instance of a specific class, to provide as an argument to a method that accepts a functional interface argument
- Outline what qualifies a Java interface as a functional interface
Key Topics
- Functional interfaces and their role in lambda expressions
- Lambda syntax and parameter types
- Method references as lambda alternatives
- Using built-in functional interfaces from java.util.function
- Stream operations with lambda expressions
- Implementing custom functional interfaces
Functional Interfaces with Lambda Expressions
Lambda expressions and interfaces
In a prior lesson, you learned about interfaces in Java such as List<E> for example. Interfaces are "contracts" that define how a class should behave. An interface's method signatures define the behaviors. The methods do not have implementations in the interface, so we call them "abstract". For example, in the List interface, get() and add() are abstract methods. Classes implement interfaces. The implementing classes must implement the interface's abstract methods, matching the method signatures. ArrayList<E> implements List<E>, and overrides the get() and add() methods (as well as the other List methods). This reading covers a group of built-in Java interfaces, called functional interfaces. We also cover a concise way to implement those interfaces with lambda expressions.
What are functional interfaces and lambda expressions?
We call any interface with exactly one abstract method a "functional interface". The List interface defines multiple methods, so it is not a functional interface. Developers can create new functional interfaces, but the Java library provides some useful functional interfaces as well. We call these the standard Java functional interfaces. We will cover four standard Java functional interfaces, focusing on the Function interface and Supplier interface. We will then learn how to use lambda expressions to implement these functional interfaces without defining a class.
Before we talk about the specifics of each functional interface, let's go over lambda expressions at a high level. Typically, implementing an interface requires writing a full class definition. The class header declares which interface it's implementing, and the class implementation overrides each abstract method. Creating and documenting an entire class could feel like a great deal of effort just to implement the single method in a functional interface. That's where lambda expressions come in handy.
As we mentioned, lambda expressions provide a quick way to implement a functional interface without explicitly defining a class or even a formal method declaration. Because each functional interface contains exactly one abstract method, lambda expressions allow us to implement only that one method to satisfy the interface. We don't need to create a class, and since the interface has only one method, we don't even need to name it!
Here's a short example of a lambda expression: value -> value == null;. The compiler converts that one line into a class that implements a functional interface. Its single method accepts one parameter (value) and returns a boolean. Imagine writing a whole class definition just for one line of code!
Since lambda expressions represent a class that implements an interface, you can assign them to a variable that references that interface. (Polymorphism!) They can also be written inline when needed. This means a lambda expression can be written anywhere you can declare or use a variable and can even be written inside another method call.
The Predicate functional interface takes one parameter and returns a boolean. The List method removeIf(Predicate filter) uses Predicate. It invokes the provided Predicate for every value stored in the List and removes it if the Predicate returns true.
If we assume a List called numberList exists and is populated with Integers, we could use our example lambda expression to remove each null value:
numberList.removeIf(value -> value == null);
It is less common, but we could also store the Predicate in a local variable, then pass it in to removeIf():
Predicate<Integer> isNull = value -> value == null;
numberList.removeIf(isNull);
If we were to write an explicit class to implement this interface, we'd need a separate class. To call it in removeIf(), we'd need to instantiate it. The examples above are equivalent to:
// CheckIfNull.java
/**
* An Integer filter/predicate that returns true when the
* provided Integer is null.
*/
public class CheckIfNull implements Predicate<Integer> {
@Override
public boolean test(Integer integer) {
return null == integer;
}
}
...
Predicate<Integer> checkIfNull = new CheckIfNull();
numberList.removeIf(checkIfNull);
That's a lot more complicated than the single-line lambda expression! Lambda expressions are often much quicker and easier to read than an entire class implementation.
The lambda expression declares a class and a method implementation; it does not invoke the method. In our example above, value is not a defined variable when we write value -> value == null, so Java can't invoke our lambda anyway. Even if a value variable existed, Java would not invoke our lambda; instead, the compiler builds the class the lambda expression defines.
An anonymous method is a method without a formal method declaration. You can define it in-line with a method body only. Lambda expressions are one example of an anonymous method. You have also created an anonymous method in your test code when calling JUnit's assertThrows() method, for example.
Anonymous methods have scope similar to Java variables. Like variables, if a lambda expression is defined at the top of a class and assigned to a variable it has class scope: any method in that class can access the lambda expression.
A lambda expression defined inside a block of code such as an if statement or for loop has block scope. A lambda expression with block scope cannot be accessed outside of the block of code in which it was declared. Curly braces define a block of code. A lambda expression written in-line such as the example above, numberList.removeIf(value -> value == null); is only available to the method it's passed to (in this case, removeIf()). Note that this is by far the most common usage.
Lambda expression usage
Now that we've discussed the use case for lambda expressions, let's talk about how to create and use one. We'll start with one example of a standard Java functional interface: Function<T,R>, then move on the others.
The Function interface
The first functional interface we want to introduce is Function<T,R>. This functional interface defines a method that accepts one parameter of type T, and returns a return value of type R. The two types can be any Java class or interface types, and can even be the same type.
The interface definition itself is:
public interface Function<T,R> {
R apply(T t);
}
Notice that this follows the same rules as other Java interfaces that we've seen. The declaration uses the keyword, interface. There is exactly one method signature and it is followed by a semicolon, with no implementation.
Before we show how a lambda expression makes implementing this interface easy, let's look at what a normal implementation would look like. We are going to use an example that assesses the value of an item, returning a String representation of the quality. If the input price is below a certain amount, we return "That's a great price!". If the input price is above a certain amount, we return "That's too expensive!" If the input price is somewhere in the middle, we will return, "That's a fair price.". Let's look at what a full class definition would look like (reminder: we'll declare the class for instructional purposes here, but we'll see how it is actually done in practice shortly):
public class PriceEvaluator implements Function<BigDecimal, String> {
public String apply(BigDecimal price) {
if (price.compareTo(new BigDecimal(50)) < 0) {
return "That's a great price!";
} else if (price.compareTo(new BigDecimal(100)) < 0) {
return "That's a fair price.";
} else {
return "That's too expensive!";
}
}
}
To implement the Function interface, the class header must declare that it will implement the interface. Since Function is a generic interface, we must also declare the types being used. We are creating a class named PriceEvaluator that implements the interface Function with BigDecimal as the input type (T) and String as the result type (R):
public class PriceEvaluator implements Function<BigDecimal, String>
Next, we must override Function's abstract method, apply(). This method takes in a BigDecimal, which we've named price, and returns a String:
public String apply(BigDecimal price) {
We can declare and instantiate PriceEvaluator like any other class:
PriceEvaluator priceEvaluator = new PriceEvaluator();
String response = priceEvaluator.apply(new BigDecimal(5));
After running this code snippet, the response would be "That's a great price!".
As we learned with, polymorphism allows us to assign the new instance to a variable that is of the Function interface type, rather than the concrete type. This still allows us to call the apply() method, as it is defined in the interface:
Function<BigDecimal, String> priceEvaluator = new PriceEvaluator();
String response = priceEvaluator.apply(new BigDecimal(5));
After running this code snippet, the response would still be "That's a great price!".
Let's now use a lambda expression instead of a separate class definition. First, we'll we assign the lambda expression to a variable, also of type Function<BigDecimal, String>:
Function<BigDecimal, String> priceEvaluator = price -> {
if (price.compareTo(new BigDecimal(50)) == -1) {
return "That's a great price!";
} else if (price.compareTo(new BigDecimal(100)) == -1) {
return "That's a fair price.";
} else {
return "That's too expensive!";
}
};
Since the class created by our lambda expression has no name, we can only declare the variable of the interface type on the left-hand side of the equal sign:
Function<BigDecimal, String> priceEvaluator
This is similar to the example above where we used a class to implement the interface and we could assign it to a variable of the Function type, Function<BigDecimal, String> priceEvaluator = new PriceEvaluator();. The change defines the lambda expression for the implementing method inline rather than in a separate class file. On the right-hand side, we define the lambda expression instead of instantiating a new PriceEvaluator(). This provides the implementation for the Function interface's apply() method that we must override. Because the functional interface has just one method, Java knows exactly which method is being overridden, even though we don't say that we're implementing apply().
There are three parts to the lambda expression:
- The method argument(s)
- The arrow separator: ->
- The method implementation (inside the curly braces)
Immediately following the equals sign above, we see parts 1 and 2:
... = price -> {
The variable price is the single argument to the apply() method. This variable is a BigDecimal. Java knows this by the first generic type declared in Function<BigDecimal, String>. The method argument can also be enclosed in parentheses like this:
... = (price) -> {
When there is only one input argument in the lambda expression, using parentheses is optional. Note that ATA's checkstyle will prevent you from using parentheses if there is only one argument. When there are multiple inputs or no input you must use parentheses.
After the input argument comes an arrow sign formed with a hyphen (-) and a greater-than sign (>). This separates the inputs from the method implementation. Following the arrow sign is an open curly brace that begins the method implementation. The curly braces are optional if you have only one line of code in your method. Since our example has multiple lines of code, we need the curly braces. The rest of the code sample is:
if (price.compareTo(new BigDecimal(50)) < 0) {
return "That's a great price!";
} else if (price.compareTo(new BigDecimal(100)) < 0) {
return "That's a fair price.";
} else {
return "That's too expensive!";
}
In this example, we have multiple return statements, which is fine for a functional interface that has a return value, just as with any method that returns a value. Some functional interfaces define methods with void return type, so they don't require return statements. Others do have return values, but the return statement is implied. We'll see examples of both later in this lesson.
Once the lambda expression is implemented, we could use it like this:
Map<BigDecimal, String> priceMap = fetchPriceMapping();
BigDecimal itemPrice = new BigDecimal("49.99");
priceMap.computeIfAbsent(itemPrice, priceEvaluator);
priceMap maps prices of items to the String "evaluation" of the price. The method, computeIfAbsent(), accepts a BigDecimal key and a Function. computeIfAbsent() first checks if the map contains the key. If the key already exists, computeIfAbsent() immediately returns. If the key does not exist in the map, computeIfAbsent() will add the key, mapping it to the value returned by calling the lambda expression, passing in itemPrice as its argument. After this code runs, priceMap would contain the entry:
<49.99, "That's a great price!">
Now, let's see how Java developers actually write this code: writing lambda expressions inline. Here, we'll pass the lambda expression directly into the computeIfAbsent() method as its second argument, rather than storing it in a local variable first. Because we're not using a variable, we don't have to declare the interface type. Here's what it looks like:
map.computeIfAbsent(itemPrice, price -> {
if (price.compareTo(new BigDecimal(50)) < 0) {
return "That's a great price!";
} else if (price.compareTo(new BigDecimal(100)) < 0) {
return "That's a fair price.";
} else {
return "That's too expensive!";
}
});
Using a lambda expression can save time and increase readability compared to the first example where we wrote a whole class to implement this interface. Now that we have walked through an example of lambda expressions using the Function interface, let's move on to another standard Java functional interface, the Supplier interface.
Supplier interface
Our next functional interface is Supplier<T>, where T is the type of the result returned. A Supplier provides an object (often by generating a new one or pulling from a data structure) each time it is called. The code for the interface is:
public interface Supplier<T> {
T get();
}
The abstract method in the Supplier functional interface is the get() method. Since get() has no method parameters, we only have one generic type, the result. It does not take input directly, but may access any variables within its scope to compute the next value to return.
Let's look at an example that generates a random index of a list. We mentioned Suppliers may use variables in its scope to help determine what value to return. In this example we see a Random and a List being used. (For instructional purposes, we'll assign a variable to the lambda.)
List<String> participants = ImmutableList.of("Ana", "Mary", "Carlos");
Random random = new Random();
Supplier<Integer> randIndexGenr = () -> random.nextInt(participants.size());
We declare a variable of the interface type, Supplier<Integer>. We've named the variable randIndexGenr (short for random index generator, to fit on one line in this reading). On the right side of the equal sign, we begin with empty parentheses: the Supplier functional interface takes no input into its get() method, so the parentheses are required and must be empty. Next, comes the arrow separator and then get()'s implementation. After the arrow, we do not have curly brackets because the body of the method is only one line and thus brackets aren't required. It doesn't appear to have a return statement, but it actually does!:
random.nextInt(participants.size());
If you drop the curly braces from your method, and the functional interface that your lambda expression is implementing has a return value, you drop the keyword return. It is most common to drop the curly braces and the return keyword, so you will often see it written that way! We could write it without this simplification for comparison (but don't do it this way!):
Supplier<Integer> randomIndexGenerator = () -> {
return random.nextInt(participants.size());
};
Now, let's look at a method that accepts a Supplier as an argument:
public void callOnParticipant(Supplier<Integer> indexChooser) {
String participant = participants.get(indexChooser.get());
System.out.println(participant + ", can you please share your answer?");
System.out.println("Thank you.");
}
As we've seen above, we can pass a lambda expression that we have stored in a variable to the method (not usually done this way):
callOnParticipant(randomIndexGenerator);
Or we can write the lambda inline (usually done this way!):
callOnParticipant(() -> random.nextInt(participants.size()));
Note: The one case where you use a variable to store the lambda expression is if you pass the identical lambda expression into multiple methods. In this case, you should not rewrite the lambda expression inline each time you want to use it. Pass your variable. The inline example is showing how to create and use a lambda expression for a single use. Because the single-use is the most common, you'll most often create and pass your lambda expressions inline.
Understanding Functional Interfaces
A functional interface in Java is an interface that contains exactly one abstract method. Lambda expressions provide a way to implement these interfaces concisely.
Here's an example of a functional interface and its implementation using lambda expression:
// Functional interface definition
@FunctionalInterface
interface StringProcessor {
String process(String input);
}
// Implementation using a lambda expression
StringProcessor reverser = (String s) -> {
StringBuilder sb = new StringBuilder(s);
return sb.reverse().toString();
};
// Usage
String result = reverser.process("Hello"); // Returns "olleH"
The key built-in functional interfaces in the java.util.function package include:
- Function<T,R>: Takes a T argument and returns an R result. Method:
R apply(T t)
- Consumer<T>: Takes a T argument and returns no result. Method:
void accept(T t)
- Supplier<T>: Takes no arguments and returns a T result. Method:
T get()
- Predicate<T>: Takes a T argument and returns a boolean. Method:
boolean test(T t)
Summary
In this reading we learned about Java's functional interfaces. We showed how a lambda expression can simplify implementing a functional interface using two different specific functional interfaces as examples. In the next reading, we will cover two more common functional interfaces (note: there are many more Java functional interfaces, but you do not need to know/memorize these; just know that they exist for now). For now, let's stop here so you can give these two functional interfaces a try.
More Functional Interfaces with Lambda Expressions
Functional interfaces continued
In the previous reading we learned about a couple different functional interfaces that are built into Java libraries and how to implement them with lambda expressions. Here we cover two more common functional interfaces, Consumer and Predicate.
Consumer interface
Next up is Consumer, which defines a method that accepts an object as input argument and returns void. Consumers are commonly used when performing an operation on each member of a data structure, or updating another data structure by calling the Consumer on each element. The Consumer<T> interface definition, where T is the type of the argument passed into the operation is:
public interface Consumer<T> {
void accept(T t);
}
The abstract method in the Consumer interface is the accept() method. This method has one parameter of type T and a void return type.
Let's look at a call to a List method, forEach(), that accepts a Consumer, calling that method with a lambda expression implementation of Consumer:
List<String> customerNames = fetchCustomerNames();
customerNames.forEach(name -> System.out.println("Customer name: " + name));
The lambda expression, name -> System.out.println("Customer name: " + name), prints out to System.out each customer's name. The lambda expression does not return any value. Since we have one input argument, we do not need parentheses around name. After the arrow, we do not have curly brackets because the body of the method is a single line. There is no return statement because the method we are implementing returns void.
The List<String> interface's forEach(Consumer<String> action) method calls the Consumer's accept(String t) method for each entry in the List, passing in the String element as the argument. We could have defined a variable that points to the lambda expression, which demonstrates that the lambda expression is a valid Consumer<String> implementation.
Consumer<String> printCustomerName = name -> System.out.println("Customer name: " + name));
(This might be helpful if we needed to pass that lambda to several different method calls, for example, if we needed to call forEach() on several different Lists. But in most cases, you'd use the inline version above)
Predicate interface
The final standard Java functional interface we'll discuss in this reading is the Predicate. A Predicate's test(T t) method accepts an object as its input argument, and returns a boolean. A Predicate is often used to filter (in or out) elements from a data structure, or to conditionally perform some operation on elements of a data structure. The functional interface definition of Predicate<T>, where T is the type of input supplied to the operation is:
public interface Predicate<T> {
boolean test(T t);
}
The abstract method in this functional interface is the test() method. This method has one parameter of type T and a boolean return type. A Predicate performs a test on an object and indicates whether it meets a specified condition or not.
Here's a call to another List method, removeIf(), that passes each element of the List to a Predicate's test(T t) method, and removes that element from the List if test(T t) returns true for that element. Here, we remove all palindromes from the List<String>, noPalindromesAllowed. The removeIf() method accepts a Predicate, calling the Predicate's test() method on each item in the list. If the item "satisfies the predicate", i.e. test(item) returns true, the item is removed from the list. In this example, if the String is a palindrome, it is removed.
List<String> noPalidromesAllowed = fetchStringsThatMayIncludePalindromes();
noPalidromesAllowed.removeIf(text -> {
String reversedString = new StringBuffer(text).reverse().toString();
return text.equals(reversedString);
});
Inside the call to removeIf(), we see our input variable, text, the arrow separator, and then test()'s implementation. With only one input argument, we do not need parentheses around the argument, text. After the arrow, we have an open curly brace, as our method is more than one line long. We then have a return statement to return the (boolean) result of the test, a closing curly brace and the parenthesis closing the removeIf() call.
For example, if fetchStringsThatMayIncludePalindromes() returned a List that included "tacocat" as one of its elements, this code would remove that element (and any other palindromes) from the List, noPalindromesAllowed.
If we stored the Predicate in a local variable to reuse across method calls, it would look like this:
Predicate<String> palindromeTest = text -> {
String reversedString = new StringBuffer(text).reverse().toString();
return text.equals(reversedString);
};
Checked exceptions in lambda expressions
Sometimes when writing a lambda expression for a standard Java functional interface, a method you're calling can potentially throw a checked exception. The code below takes a string containing a file name, and opens the file for reading data.
Function<String, FileInputStream> fileOpener = filename -> {
File file = new File(filename);
return new FileInputStream(file);
}
The FileInputStream constructor can throw a FileNotFoundException, which is a checked exception (it inherits from Exception via IOException, not RuntimeException) Checked exceptions must be handled or the method must include a throws clause in the method declaration. A lambda expression doesn't have a method declaration to add a throws clause. Further, Function's apply() method doesn't declare throws for any exceptions, so our implementation cannot either (throws clauses are part of a method's signature).
Java doesn't allow lambda expressions to declare throws because they are intended to be concise implementations of a single method interface. Recall from the Polymorphism and Interfaces lesson that when you implement an interface, your methods can only throw narrower exceptions than the method they override. Take a look back at each of the standard Java functional interfaces. None of the methods they define declare checked exceptions.
Our other option when handling checked exceptions is to surround them with a try-catch. In the catch block, you may choose to return a special value or throw an unchecked exception. Lambda expressions can't throw checked exceptions, but they can throw unchecked exceptions. Instead of throwing the checked exception we could wrap the exception in an unchecked exception, IllegalArgumentException:
Function<String, FileInputStream> fileOpener = filename -> {
File file = new File(filename);
try {
return new FileInputStream(file);
} catch (FileNotFoundException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
};
We could also choose to ignore the not-found files, perhaps logging them for future follow-up (but forcing the calling code to deal with a null return value, which can cause its own challenges):
Function<String, FileInputStream> fileOpener = filename -> {
File file = new File(filename);
try {
return new FileInputStream(file);
} catch (FileNotFoundException e) {
logger.warn("Can't open missing file: " + filename);
return null;
}
};
Summary
Functional interfaces are interfaces that contain only one abstract method. Java contains several standard functional interfaces that you can use and implement. The interfaces covered in the last two readings are Function, Supplier, Consumer, and Predicate, but there are many more standard functional interfaces. You use lambda expressions to avoid writing a full class definition that implements a functional interface. Lambda expressions provide a concise way to implement these interfaces. Lambda expressions are frequently created inline inside method calls.
When writing lambda expressions, be sure you plan for handling checked exceptions. Since lambda expressions cannot throw checked exceptions, one way to handle this is to throw a RuntimeException (subclass) that is a wrapper for the checked exception.
Before learning about functional interfaces, you were capable of solving each of the problems presented in this lesson, but you would likely have accomplished their solution by writing longer code. The examples in this lesson make use of of a different style of programming called functional programming. We'll continue to see examples written in this style and use more methods that accept functional interfaces as parameters.
Method References
Overview
Sometimes a lambda expression does nothing but call an existing method. In those cases, it's often clearer to refer to the existing method by name. Method references enable you to do this; they are compact, easy-to-read lambda expressions for methods that already have a name.
A method reference use two colons to separate the class type or object from the method.
In this reading, we will cover three types of method references:
- Reference to an instance method of a particular object
format: containingObject::instanceMethodName - Reference to an instance method of an arbitrary object of a particular type
format: ContainingType::methodName - Reference to a static method
format: ContainingClass::staticMethodName
Reference to an Instance Method of a Particular Object
When creating a CacheLoader for a Guava cache, we use the static factory method CacheLoader.from() which accepts a Function. This function tells the CacheLoader how to retrieve data in the case of a cache miss.
For our example, let DelegateDao be defined as:
public class DelegateDao {
public String getDataFromDatabase(String key) {
// implementation excluded
}
}
We'll define our CachingDao below using a lambda expression without a method reference:
public CachingDao {
private final LoadingCache<String, String> cache;
public CachingDao(final DelegateDao delegateDao) {
this.cache = CacheBuilder.newBuilder()
.build(CacheLoader.from( key -> delegateDao.getDataFromDatabase(key) );
}
Since our lambda expression does nothing but provide an existing method as the function input, this is an opportunity for a method reference! We can replace our lambda expression with code that follows the pattern containingObject::instanceMethodName.
Our updated CachingDao looks like:
public CachingDao {
private final LoadingCache<String, String> cache;
public CachingDao(final DelegateDao delegateDao) {
this.cache = CacheBuilder.newBuilder()
.build( CacheLoader.from(delegateDao::getDataFromDatabase );
}
Note: we don't have any input variable in our new lambda expression that represents a key in the cache. We still have input to our Function, Java is just smart enough to expand the method reference to something like the longer lambda expression: key -> delegateDao.getDataFromDatabase(key).
This case differs from the case below in that we need an object to call the method we are referencing. The input to our Function is being used as a argument to the method we are referencing. In the below case, we are calling the method we are referencing on the input object itself.
Reference to an Instance Method of an Arbitrary Object of a Particular Type
Suppose we have a List of String objects, and we want to remove all empty Strings from the List.
The removeIf method will remove all elements of a collection that satisfy a given Predicate. It will pass each item in the list to the Predicate to be evaluated.
We'll use the messages list in our examples below:
List<String> messages = Arrays.asList("This message...", "", "", "Hi all - today I...", "WOW! This is so...", "");
We might write the code to remove empty Strings like this:
messages.removeIf( message -> message.isEmpty() );
We are utilizing the String object method isEmpty.
Since our lambda expression does nothing but provide an existing method as the predicate input, this is an opportunity for a method reference! We can replace our lambda expression with code that follows the pattern ContainingClass::methodName.
messages.removeIf( String::isEmpty() );
Note: we don't have any input variable in our new lambda expression that represents an item from the messages list. We still have input to our Predicate, Java is just smart enough to expand the method reference to something like the longer lambda expression: message -> message.isEmpty().
Reference to a Static Method
Suppose we have a List of String objects, and we want to remove, not only empty but also blank or null values in our list. Apache's StringUtils class provides a static method isBlank that checks is the provided String is empty (""), null, or whitespace only (" ").
The removeIf method will remove all elements of a collection that satisfy a given Predicate. It will pass each item in the list to the Predicate to be evaluated.
We'll use the strings list in our examples below:
List<String> strings = Arrays.asList("good string", "", " ", "Hi", "Stringy", null, "");
We might write the code to remove invalid Strings like this:
strings.removeIf( s -> StringUtils.isBlank(s) );
We are utilizing the StringUtils static method isBlank.
Since our lambda expression does nothing but provide an existing method as the predicate input, this is an opportunity for a method reference! We can replace our lambda expression with code that follows the pattern ContainingClass::staticMethodName.
strings.removeIf(StringUtils::isBlank);
Note: we don't have any input variable in our new lambda expression that represents an item from the strings list. We still have input to our Predicate, Java is just smart enough to expand the method reference to something like the longer lambda expression: s -> StringUtils.isBlank(s).
Summary
Method references provide a way to write compact lambda expressions that simply reference an existing Java method. They can be used when you are calling an existing method on the input to your lambda expression, or when you are passing your input to an existing method.
Advanced Lambda Expressions
Method References
Method references provide an even more concise way to express lambda expressions that simply call an existing method.
There are four types of method references:
// 1. Reference to a static method
Function<String, Integer> parser = Integer::parseInt;
// 2. Reference to an instance method of a particular object
String str = "Hello";
Supplier<Integer> lengthSupplier = str::length;
// 3. Reference to an instance method of an arbitrary object of a particular type
Function<String, Integer> lengthFunc = String::length;
// 4. Reference to a constructor
Supplier<List<String>> listSupplier = ArrayList::new;
Lambda Expressions with Streams
Java's Stream API works seamlessly with lambda expressions to process collections of objects:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Dave");
// Filter names starting with 'A'
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
// Transform names to uppercase
List<String> upperNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// Find any name longer than 5 characters
Optional<String> longName = names.stream()
.filter(name -> name.length() > 5)
.findAny();
// Combine multiple operations
double averageLength = names.stream()
.filter(name -> name.length() > 3)
.mapToInt(String::length)
.average()
.orElse(0);