DEV Community

Cover image for Java lambda expression tutorial: Functional programming in Java
Ryan Thelin for Educative

Posted on • Originally published at educative.io

Java lambda expression tutorial: Functional programming in Java

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:

Reskill to a Java Developer

Learn hirable Java skills fast with hands-on practice.

Java for Programmers



What are lambda expressions?

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
Enter fullscreen mode Exit fullscreen mode

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 use cases in Java

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.

How to write a lambda expression in Java

As we saw earlier, the basic form of a lambda expression is passed a single parameter.

parameter -> expression
Enter fullscreen mode Exit fullscreen mode

A single lambda expression can also have multiple parameters:

    (parameter1, parameter2) -> expression
Enter fullscreen mode Exit fullscreen mode

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); } );
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Lambda Function Body

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);
  }
Enter fullscreen mode Exit fullscreen mode

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
   }
Enter fullscreen mode Exit fullscreen mode

Lambda even has its own return statement:

(a, b) -> a + b;
Enter fullscreen mode Exit fullscreen mode

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.

Lambdas as Objects

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.

// WellWisher.java
public class WellWisher {
    public static void wish(Greeting greeting) {
        greeting.greet();
    }
    // Passing a lambda expression to the wish method
    public static void main(String args[]) {
        wish( () -> System.out.println("Namaste") );
    }
}
Enter fullscreen mode Exit fullscreen mode
// Greeting.java
@FunctionalInterface
public interface Greeting {
    void greet();
}
Enter fullscreen mode Exit fullscreen mode

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.

Keep learning about modern Java.

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.

Java for Programmers

Interfaces in Java

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.
}
Enter fullscreen mode Exit fullscreen mode

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 java8

class Test 
{ 
    public static void main(String args[]) 
    { 
        // create anonymous inner class object 
        new Thread(new Runnable() 
        { 
            @Override
            public void run() // anonymous class
            { 
                System.out.println("New thread created"); 
            } 
        }).start(); 
    } 
} 
Enter fullscreen mode Exit fullscreen mode
// functional interface using lambda expressions 

class Test 
{ 
  public static void main(String args[]) 
  { 

    // lambda expression to create the object 
    new Thread(()-> 
       {System.out.println("New thread created");}).start(); 
  } 
} 
Enter fullscreen mode Exit fullscreen mode

Functional Interfaces

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); 
} 
Enter fullscreen mode Exit fullscreen mode

Default methods in interfaces

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Static methods in interfaces

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.

// Car.java
public abstract class Car implements Vehicle {
    public static void repair(Vehicle vehicle){
        vehicle.repairVehicle();
    }
    public static void main(String args[]){

        Vehicle.cleanVehicle(); //This will compile.
        Car.repair(() -> System.out.println("Car repaired"));
    }
}
Enter fullscreen mode Exit fullscreen mode
// Vehicle.java
//functional interface
public interface Vehicle {
    static void cleanVehicle(){
        System.out.println("I am cleaning vehicle");
    }
    void repairVehicle();
}
Enter fullscreen mode Exit fullscreen mode

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.

What to learn next

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:

  • Stream API
  • Concurrency API additions
  • Bulk Data handling tools
  • Built-in higher-order functions

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!

Continue learning about Java 8 and functional programming

Top comments (0)