Java Lambda expressions and Functional interfaces.

Shihara Dilshan
8 min readJul 28, 2023

--

Java 8 brought a lot of new feature to the language. It was a huge update that Java ever had. Among all those updates Lambda expressions and functional interfaces are very interesting features. So will have a look at those in this article. Also I have a introduction article regarding these topics in here. https://medium.com/@shiharadilshan/java-8-functional-interfaces-and-stream-api-56902b5661bf

What is a functional interface?

Functional interface is just a interface in java but only have 1 abstract method. Interfaces can have many abstract methods but in functional interface there is only one.

How a functional interface is different than a normal interface?

The main difference is you can you Lambda to instantiate a functional interface. That means you don’t need a concrete class to implement the functional interface.

interface NormalInterface{
void methode1();
void methode2();
}

interface FunctionalInterface{
void methode1();
}

class TestClass{
public void useNormalInterface(NormalInterface normalInterface){
System.out.println("I am accepting an NormalInterface referencing object");
}

public void useFunctionalInterface(FunctionalInterface functionalInterface){
System.out.println("I am accepting an FunctionalInterface referencing object");
}
}

public class Main {
public static void main(String[] args) {
TestClass testClass= new TestClass();

testClass.useNormalInterface(new NormalInterface() {
@Override
public void methode1() {
System.out.println("I can't use lambda expressions");
}

@Override
public void methode2() {
System.out.println("I have to use ether anonymous class or a concrete class");
}
});

testClass.useFunctionalInterface( () -> {
System.out.println("I can use lambda expressions");
});
}
}

In the above example I have two interfaces.

  1. Normal interface with several abstract methods.
  2. Functional interface with one abstract method.

Since you cannot instantiate an interface you have to ether implement the interface in a concrete class or you can your anonymous class. But since we have an functional interface we can use lambda expressions.

Why do we need functional interfaces?

By using functional interfaces we can abstractions that can be used within multiple places without repeating the code.

Yeah I know 🙂. So let’s move into a practical example.

Let’s consider an example where we want to perform different mathematical operations on two numbers. We’ll create a functional interface called MathOperation that represents a mathematical operation that takes two integers as input and returns an integer as the result.

@FunctionalInterface
interface MathOperation {
int operate(int a, int b);
}

Now, we can create implementations of this interface for various mathematical operations without using lambda expressions.

class Addition implements MathOperation {
public int operate(int a, int b) {
return a + b;
}
}

class Subtraction implements MathOperation {
public int operate(int a, int b) {
return a - b;
}
}

class Division implements MathOperation {
public int operate(int a, int b) {
return a / b;
}
}

class Multiplication implements MathOperation {
public int operate(int a, int b) {
return a * b;
}
}

public class Calculator {
public static void main(String[] args) {
int operand1 = 10;
int operand2 = 5;

MathOperation add = new Addition();
MathOperation subtract = new Subtraction();
MathOperation multiply = new Multiplication();
MathOperation division = new Division();

int result1 = add.operate(operand1, operand2);
int result2 = subtract.operate(operand1, operand2);
int result3 = multiply.operate(operand1, operand2);
int result3 = division.operate(operand1, operand2);

System.out.println("Addition result: " + result1);
System.out.println("Subtraction result: " + result2);
System.out.println("Multiplication result: " + result3);
System.out.println("Division result: " + result4);
}
}

Now, let’s see how we can use these implementations to perform different mathematical operations without duplicating code with the help of lambda expressions.

public class Calculator {
public static void main(String[] args) {
MathOperation addition = (a, b) -> a +b;
MathOperation substraction = (a, b) -> a -b;
MathOperation multiplication = (a, b) -> a * b;
MathOperation divide = (a, b) -> a / b;

int result1 = addition.operate(operand1, operand2);
int result2 = substraction.operate(operand1, operand2);
int result3 = multiplication.operate(operand1, operand2);
int result4 = divide.operate(operand1, operand2);

System.out.println("Addition result: " + result1);
System.out.println("Subtraction result: " + result2);
System.out.println("Multiplication result: " + result3);
System.out.println("Division result: " + result4);
}
}

These are some example of custom functional interfaces. Java provide some functional interfaces in java.util.function. So we can use those in suitable use cases. Let’s deep dive into some functional interfaces which provided by Java.

  1. Predicate/BiPredicate

Predicate

A function that accepts a type T input and returns a boolean value (true or false) is represented by the Predicate interface. This is how it is defined inside the java.util.function.

@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}

The Predicate interface’s test function accepts a type T argument and returns true if the provided condition is met for that input or false if it is not. Predicates are frequently used to sort collections, confirm assumptions, or assess standards.

Example usage of Predicate:

Predicate<Integer> isEven = num -> num % 2 == 0;
boolean result = isEven.test(10);

BiPredicate

A function that accepts two inputs of the types T and U and returns a boolean value is represented by the BiPredicate interface. The only difference between Predicate and BiPredicate is that Predicate takes one argument but BiPredicate and work with two arguments. This is how it is defined inside the java.util.function.

@FunctionalInterface
public interface BiPredicate<T, U> {
boolean test(T t, U u);
}

The test method of the BiPredicate interface accepts two arguments of types T and U and returns true if the provided condition is met for those inputs or false in the absence of that condition. When we need to test a condition that incorporates two variables, BiPredicate can be helpful.

Example usage of BiPredicate:

BiPredicate<Integer, Integer> areEqual = (num1, num2) -> num1.equals(num2);
boolean result = areEqual.test(5, 5);

2. Supplier

The Supplier functional interface represents a supplier of results, meaning it doesn’t take any input arguments but produces a result of a specified type. The Supplier interface is defined as follows.

@FunctionalInterface
public interface Supplier<T> {
T get();
}

As you can see, it’s a generic interface where T stands for the kind of output the supplier produces. The only method in the Supplier interface is get(), which returns a result of type T and requires no arguments.

The supplier sends the output via the get() method. This technique is frequently used to output values instantly, without the need for human input. It is frequently used in situations where a value is required but no input arguments are required to obtain that value.

Using Supplier as an example :

Supplier<Integer> randomNumberSupplier = () -> (int) (Math.random() * 100);
int randomValue = randomNumberSupplier.get();
System.out.println("Random number: " + randomValue);

generate a random number between 0 and 99. The get() method is called on the randomNumberSupplier , and it provides a new random number every time it's called.

3. Consumer/BiConsumer

@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}

These are used to represent operations that consume (take in) values of certain types and perform some actions without returning any result. The only difference between Consumer and BiConsumer is the number of arguments it can takes.

List<String> fruits = Arrays.asList("Apple", "Banana", "Orange");
Consumer<String> printFruit = fruit -> System.out.println(fruit);


Map<String, Integer> scores = new HashMap<>();
scores.put("John", 80);
scores.put("Alice", 95);
scores.put("Bob", 70);
BiConsumer<String, Integer> printScore = (name, score) -> System.out.println(name + ": " + score);

4. Function/BiFunction

Function : A Function that accepts a type T input and outputs a type R result is represented by the Function interface. This is how it is defined.

@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}

BiFunction : A function that accepts two inputs of types T and U and outputs a result of type R is represented by the BiFunction interface. This is how it is defined.

@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}

example usage :

Function<String, Integer> stringLengthFunction = str -> str.length();
int length = stringLengthFunction.apply("Hello");
System.out.println("Length of the string: " + length);


BiFunction<String, String, String> concatenateFunction = (str1, str2) -> str1 + " " + str2;
String result = concatenateFunction.apply("Hello", "world");
System.out.println("Concatenated string: " + result);

The only difference between Function and BiFunction is that the number of argument it can takes.

5. UnaryOperator/BinaryOperator

The functional interfaces UnaryOperator and BinaryOperator, respectively, extend the Function and BiFunction interfaces. They stand in for particular situations of functions whose input and output are of the same type.

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
// Inherits the 'apply' method from the Function interface
}

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T, T, T> {
// Inherits the 'apply' method from the BiFunction interface
}

6. Method references

When constructing lambda expressions in Java and the body of the expression just contains one method call, method references are a convenient shorthand notation to use. They offer terms like Function, Consumer, Predicate, and others that make it easier to refer to existing methods and use them as functional interfaces. Method references are frequently used in order to streamline and make code easier to read.

In Java, there are four different kinds of method references:

  • Reference to a static method.
  • Reference to an instance method.
  • Reference to a constructor
  1. Reference to a static method.

You can make use of the class’s static methods. The syntax and an example that demonstrate how to refer to a static method in Java are provided below.

import java.util.function.Predicate;

public class MethodReferenceExample {
public static boolean isEven(int number) {
return number % 2 == 0;
}

public static void main(String[] args) {
// Using a method reference to a static method
Predicate<Integer> isEvenPredicate = MethodReferenceExample::isEven;
System.out.println(isEvenPredicate.test(6)); // Output: true
System.out.println(isEvenPredicate.test(5)); // Output: false
}
}

2. Reference to an instance method.

You can also refer to instance methods, much like static methods. In the example that follows.

import java.util.function.Predicate;

public class MethodReferenceExample {
public boolean isEven(int number) {
return number % 2 == 0;
}

public static void main(String[] args) {
MethodReferenceExample instance = new MethodReferenceExample();

// Using a method reference to an instance method of a particular object
Predicate<Integer> isEvenPredicate = instance::isEven;
System.out.println(isEvenPredicate.test(6)); // Output: true
System.out.println(isEvenPredicate.test(5)); // Output: false
}
}

3. Reference to a constructor

We can refer a constructor by using the new keyword.

import java.util.function.Function;

public class MethodReferenceExample {
private String message;

public MethodReferenceExample(String message) {
this.message = message;
}

public String getMessage() {
return message;
}

public static void main(String[] args) {
// Using a method reference to a constructor
Function<String, MethodReferenceExample> constructorReference = MethodReferenceExample::new;

// Create an instance using the constructor reference
MethodReferenceExample instance = constructorReference.apply("Hello, Method Reference!");
System.out.println(instance.getMessage()); // Output: Hello, Method Reference!
}
}

Practical scenarios for using lambda expressions and functional interfaces.

  1. Runnable interface
  2. Comparator interface
  3. Action listeners in GUI programming
  4. Functional programming with Stream API
  5. Functional interfaces as method parameters

That’s it. I hope you learned something new.

--

--

Shihara Dilshan
Shihara Dilshan

Written by Shihara Dilshan

Associate Technical Lead At Surge Global | React | React Native | Flutter | NodeJS | AWS | Type Script | Java Script | Dart | Go

No responses yet