-Readability: For those unfamiliar with the syntax, lambda expressions can make code harder to read and understand. -Debugging: Stack traces involving lambdas can be more complex, making debugging slightly more challenging.
With over 60% of professional developers still using Java 8 in the beginning of 2021, understanding the features of Java 8 is an essential skill. Java 8 was released in 2014, bringing with it a heap of new features.
Among these changes were features that allowed Java developers to write in a functional programming style. One of the biggest changes was the addition of lambda expressions.
Lambdas are similar to methods, but they do not need a name and can be implemented outside of classes. As a result, they open the possibility for fully functional programs and pave the way for more functional support from Java in the future.
Today, we’ll help you get started with lambda expressions and explore how they can be used with interfaces.
Here’s what we’ll cover today:
Learn hirable Java skills fast with hands-on practice.
Lambda expressions are an anonymous function, meaning that they have no name or identifier. They can be passed as a parameter to another function. They are paired with a functional interface and feature a parameter with an expression that references that parameter.
The syntax of a basic lambda expression is:
parameter -> expression
The expression is used as the code body for the abstract method (a named but empty method) within the paired functional interface.
Unlike most functions in Java, lambda expressions exist outside of any object’s scope. This means they are callable anywhere in the program and can be passed around. In the simplest terms, lambda expressions allow functions to behave like just another piece of data.
Lambda expressions are used to achieve the functionality of an anonymous class without the cluttered implementation. They’re great for repeating simple behaviors that could be used in multiple areas across the program, for example, to add two values without changing the input data.
These properties make lambda especially useful for functional programming styles in Java. Before Java 8, Java struggled to find tools to meet all the principles of functional programming.
Functional programming has 5 key principles:
Pure functions: Functions that operate independently from the state outside the function and contain only operations that are essential to find the output.
Immutability: Inputs are referenced, not modified. Functions should avoid complex conditional behavior. In general, all functions should return the same value regardless of how many times it is called.
First-class functions: Functions are treated the same as any other value. You can populate arrays with functions, pass functions as parameters, etc.
Higher-order functions: Higher-order functions either one or more functions as parameters or return a function. These are essential to creating complex behaviors with functional programming.
Function Composition: Multiple simple functions can be strung together in different orders to create complex functions. Simple functions complete a single step that may be shared across multiple tasks, while complex functions complete an entire task.
Lambda expressions help us achieve pure functions, immutability, and first-class functions principles in Java.
Lambda functions are pure because they do not rely on a specific class scope. They are immutable because they reference the passed parameter but do not modify the parameter’s value to reach their result. Finally, they’re first-class functions because they can be anonymous and passed to other functions.
Lambda expressions are also used as event listeners and callback functions in non-functional programs because of their class independence.
As we saw earlier, the basic form of a lambda expression is passed a single parameter.
parameter -> expression
A single lambda expression can also have multiple parameters:
(parameter1, parameter2) -> expression
The expression segment, or lambda body, contains a reference to the parameter. The value of the lambda expression is the value of the expression when executed with the passed parameters.
For example:
import java.util.ArrayList;public class main {public static void main(String[] args) {ArrayList<Integer> numbers = new ArrayList<Integer>();numbers.add(5);numbers.add(9);numbers.add(8);numbers.add(1);numbers.forEach( (n) -> { System.out.println(n); } );}}
The parameter n
is passed to the expression System.out.println(n)
. The expression then executes using the value of the parameter n
in the print statement. This repeats for each number in the ArrayList, passing each element in the list into the lambda expression as n
. The output of this expression is therefore a printed list of the ArrayList’s elements: 5 9 8 1
.
The lambda function body can contain expressions over multiple lines if encased in curly braces.
For example:
(oldState, newState) -> {
System.out.println("Old state: " + oldState);
System.out.println("New state: " + newState);
}
This allows for more complex expressions that execute code blocks rather than a single statement.
You can also return from lambda functions by adding a return statement within the function body.
public static Addition getAddition() {
return (a, b) -> a + b; // lambda expression return statement
}
Lambda even has its own return statement:
(a, b) -> a + b;
The compiler assumes that a+b
is our return value. This syntax is cleaner and will produce the same output as the previous example.
Regardless of how long or complex the expression gets, remember that lambda expressions must immediately output a consistent value. This means an expression cannot contain any conditional statements like if
or while
and cannot wait for user input.
All code within the expression must have an immutable output regardless of how many times it is run.
You can send lambdas to other functions as parameters. Imagine that we want to create a greeting program that is open for more greeting
functions to be added in different languages.
@FunctionalInterfacepublic interface Greeting {void greet();}
Here the expression itself is passed, and the greet();
function is immediately executed. From here, we can add additional greet functions for different languages that will override to print only the correct greeting.
Java is still one of the most sought after languages by modern companies. Educative’s Paths give you all the hands-on practice you need to reskill in half the time.
Interfaces in Java are similar to classes. They are blueprints that contain variables and methods. However, interfaces contain only abstract methods that have signatures but no code implementation.
Interfaces can be thought of as a list of attributes or methods that an implementing class must define to operate. The interface says what features it must have but not how to implement them.
For example, you might have an interface Character
that lists methods for all the things a character in a video game must be able to do. The interface lists that all characters must have a move()
method but leaves it up to the class of the individual characters to define the distance and means (flight, running, sliding, etc.) of movement.
The syntax of an interface is:
interface <interface_name> {
// declare constant fields
// declare methods that abstract
// by default.
}
With interfaces, Java classes achieve multiple inheritances since they are not applied to the one class inheritance limit. It also helps us achieve total abstraction since the interface holds no scope or values by default.
Lambda expressions are used to express an instance of these interfaces. Before Java 8, we had to create an inner anonymous class to use these interfaces.
// functional interface before java8class Test{public static void main(String args[]){// create anonymous inner class objectnew Thread(new Runnable(){@Overridepublic void run() // anonymous class{System.out.println("New thread created");}}).start();}}
Lambda expressions can only implement functional interfaces, which is an interface with only one abstract method. The lambda expression essentially provides the body for the abstract method within the functional interface.
If the interface had more than one abstract method, the compiler would not know which method should use the lambda expression as its body. Common examples of built-in functional interfaces are Comparator
or Predicate
.
It’s best practice to add the optional @FunctionalInterface
annotation to the top of any functional interface.
Java understands the annotation as a restriction that the marked interface can have only one abstract method. If there is more than a single method, the compiler will send an error message.
Using the annotation ensures that there is no unexpected behavior from lambda expressions that call this interface.
@FunctionalInterface
interface Square
{
int calculate(int x);
}
While functional interfaces have a limit on abstract methods, there is no limit on default or static methods. Default or static methods can fine-tune our interfaces to share different behaviors with inheriting classes.
Default methods can have a body within an interface. Most importantly, default methods in interfaces to provide additional functionality to a given type without breaking down the implementing classes.
Before Java 8, if a new method was introduced in an interface, all the implementing classes would break. To fix it, we would need to individually provide the implementation of that method in all the implementing classes.
However, sometimes methods have only a single implementation, and there is no need to provide their implementation in each class. In that case, we can declare that method as a default in the interface and provide its implementation in the interface itself.
public interface Vehicle {
void cleanVehicle();
default void startVehicle() {
System.out.println("Vehicle is starting");
}
}
Here, the default method is startVehicle()
while cleanVehicle()
is abstract. Regardless of the implementing class, startVehicle()
will always print the same phrase. Since the behavior does not change based on the class, we can simply use the default method to avoid repeated code.
Most importantly, the Vehicle
interface still only has 1 abstract method and therefore is counted as a functional interface that can be used with lambda expressions.
The static methods in interfaces are similar to default methods, but they cannot be overridden. Static methods are great when you want a method’s implementation to be unchangeable by implementing classes.
//functional interfacepublic interface Vehicle {static void cleanVehicle(){System.out.println("I am cleaning vehicle");}void repairVehicle();}
In the Car
class, we’re able to call cleanVehicle()
to produce the implementation defined in our interface. If we attempt to @Override
the cleanVehicle()
method, we’ll get an error message because it was declared static
.
Finally, we can still use this interface in our lambda expressions because repairVehicle()
is our only abstract method.
Lambda functions are one of the most useful additions with Java 8. However, there are many more features that make Java 8 the most popular language used by professional developers.
Some features to learn next are:
To help you master Java 8 and brush up on your Java skills, we’ve assembled the Java for Programmers Learning Path. These curated modules cover all the pillars of Java programming like OOP, multithreading, recursion, and deep dives on all the major changes in Java 8.
By the end, you’ll have hands-on experience with the skills that modern interviews are looking for.
Happy learning!
Free Resources