Introduction
In this article, using examples, we will explore lambda expressions in Java, their use with functional interfaces, parameterized functional interfaces, and Stream APIs.
Lambda expressions were added in Java 8. Their main purpose is to improve readability and reduce the amount of code.
But before moving on to lambdas, we need to understand functional interfaces.
What is a functional interface?
If an interface in Java contains one and only one abstract method, then it is called functional. This single method determines the purpose of the interface.
For example, the Runnable interface from the java.lang package is functional because it contains only one run () method.
Example 1: declaring a functional interface in java
import java.lang.FunctionalInterface;
@FunctionalInterface
public interface MyInterface{
//
double getValue();
}
In the above example, the MyInterface interface has only one abstract method, getValue (). So this interface is functional.
Here we have used annotationFunctionalInterfacewhich helps the compiler understand that the interface is functional. Hence it does not allow for more than one abstract method. However, we can omit it.
In Java 7, functional interfaces were considered Single Abstract Methods (SAM). SAMs were usually implemented using anonymous classes.
Example 2: implementing SAM with anonymous class in java
public class FunctionInterfaceTest {
public static void main(String[] args) {
//
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(" Runnable.")
}
}).start();
}
}
Execution result:
Runnable.
In this example, we are accepting an anonymous class to call the method. This helped write programs with fewer lines of code in Java 7. However, the syntax remained quite complex and cumbersome.
Java 8 has expanded the capabilities of SAM, taking it a step further. As we know, the functional interface contains only one method, therefore, we do not need to specify the name of the method when passing it as an argument. This is exactly what lambda expressions allow us to do.
Introduction to lambda expressions
Lambda expressions are essentially an anonymous class or method. The lambda expression does not execute on its own. Instead, it is used to implement the method defined in the functional interface.
How do I write a lambda expression in Java?
In Java, lambda expressions have the following syntax:
(parameter list) -> lambda body
Here we have used a new operator (->) - the lambda operator. Perhaps the syntax seems a little tricky. Let's take a couple of examples.
Let's say we have a method like this:
double getPiValue() {
return 3.1415;
}
We can write it using a lambda like:
() -> 3.1415
This method has no parameters. Therefore, the left side of the expression contains empty parentheses. The right side is the body of the lambda expression, which defines its action. In our case, the return value is 3.1415.
Types of lambda expressions
In Java, the body of a lambda can be of two types.
1. One-line
() -> System.out.println("Lambdas are great");
2. Block (multi-line)
() -> {
double pi = 3.1415;
return pi;
};
This type allows a lambda expression to have multiple operations internally. These operations must be enclosed in curly braces, followed by a semicolon.
Note: Multi-line lambda expressions must always have a return statement, unlike single-line ones.
Example 3: Lambda Expression
Let's write a Java program that returns Pi using a lambda expression.
As stated earlier, a lambda expression does not automatically execute. Rather, it forms an implementation of an abstract method declared in a functional interface.
And so, first, we need to describe the functional interface.
import java.lang.FunctionalInterface;
//
@FunctionalInterface
interface MyInterface{
//
double getPiValue();
}
public class Main {
public static void main( String[] args ) {
// MyInterface
MyInterface ref;
// -
ref = () -> 3.1415;
System.out.println("Value of Pi = " + ref.getPiValue());
}
}
Execution result:
Value of Pi = 3.1415
In the example above:
- We have created a functional interface, MyInterface, which contains one abstract method, getPiValue ().
- Inside the Main class, we have declared a reference to MyInterface. Note that we can declare an interface reference, but we cannot create an object of it.
// MyInterface ref = new myInterface(); // MyInterface ref;
- Then we assigned the lambda expression to the link
ref = () -> 3.1415;
- Finally, we called the getPiValue () method using the interface reference.
System.out.println("Value of Pi = " + ref.getPiValue());
Lambda expressions with parameters
Up to this point, we have been creating lambda expressions without any parameters. However, just like methods, lambdas can have parameters.
(n) -> (n % 2) == 0
In this example, the variable n within the parentheses is the parameter passed to the lambda expression. The lambda body takes a parameter and checks it for parity.
Example 4: using a lambda expression with parameters
@FunctionalInterface
interface MyInterface {
//
String reverse(String n);
}
public class Main {
public static void main( String[] args ) {
// MyInterface
// -
MyInterface ref = (str) -> {
String result = "";
for (int i = str.length()-1; i >= 0 ; i--)
result += str.charAt(i);
return result;
};
//
System.out.println("Lambda reversed = " + ref.reverse("Lambda"));
}
}
Execution result:
Lambda reversed = adbmaL
Parameterized functional interface
Up to this point, we have used functional interfaces that only accept one value type. For instance:
@FunctionalInterface
interface MyInterface {
String reverseString(String n);
}
The above functional interface only accepts String and returns String. However, we can make our interface generic to use with any data type.
Example 5: Parameterized Interface and Lambda Expressions
//
@FunctionalInterface
interface GenericInterface<T> {
//
T func(T t);
}
public class Main {
public static void main( String[] args ) {
//
// String
//
GenericInterface<String> reverse = (str) -> {
String result = "";
for (int i = str.length()-1; i >= 0 ; i--)
result += str.charAt(i);
return result;
};
System.out.println("Lambda reversed = " + reverse.func("Lambda"));
//
// Integer
//
GenericInterface<Integer> factorial = (n) -> {
int result = 1;
for (int i = 1; i <= n; i++)
result = i * result;
return result;
};
System.out.println("factorial of 5 = " + factorial.func(5));
}
}
Execution result:
Lambda reversed = adbmaL
factorial of 5 = 120
In this example, we have created a parameterized functional interface GenericInterface that contains a parameterized func () method.
Then, inside the Main class:
- GenericInterface <String> reverse - creates a link to an interface that works with String.
- GenericInterface <Integer> factorial - Creates a link to an interface that works with Integer.
Lambda Expressions and Stream API
JDK8 adds a new package, java.util.stream, which allows java developers to perform operations such as searching, filtering, matching, merging, or manipulating collections such as Lists.
For example, we have a data stream (in our case, a list of strings), where each string contains the name of a country and its city. Now we can process this data stream and select only the cities of Nepal.
To do this, we can use a combination of Stream API and lambda expressions.
Example 6: Using Lambdas in the Stream API
import java.util.ArrayList;
import java.util.List;
public class StreamMain {
//
static List<String> places = new ArrayList<>();
//
public static List getPlaces(){
//
places.add("Nepal, Kathmandu");
places.add("Nepal, Pokhara");
places.add("India, Delhi");
places.add("USA, New York");
places.add("Africa, Nigeria");
return places;
}
public static void main( String[] args ) {
List<String> myPlaces = getPlaces();
System.out.println("Places from Nepal:");
//
myPlaces.stream()
.filter((p) -> p.startsWith("Nepal"))
.map((p) -> p.toUpperCase())
.sorted()
.forEach((p) -> System.out.println(p));
}
}
Execution result:
Places from Nepal:
NEPAL, KATHMANDU
NEPAL, POKHARA
In the example above, notice this expression:
myPlaces.stream()
.filter((p) -> p.startsWith("Nepal"))
.map((p) -> p.toUpperCase())
.sorted()
.forEach((p) -> System.out.println(p));
Here we use methods like filter (), map (), forEach () from the Stream API, which can take lambdas as a parameter.
Also, we can describe our own expressions based on the syntax described above. This will allow us to reduce the number of lines of code.