Lesson 2: Functional Interfaces

Now that you understand lambda expressions, let’s explore functional interfaces, which are essential for using lambdas effectively.


1. What is a Functional Interface?

A functional interface is an interface that contains exactly one abstract method. It may have multiple default or static methods, but only one abstract method is required for it to be considered functional.

Why Functional Interfaces?

  • They enable the use of lambda expressions.
  • They make code concise and easier to read.
  • They support functional programming in Java.

2. @FunctionalInterface Annotation

Java provides the @FunctionalInterface annotation to ensure that an interface meets the functional interface rules. If an interface with this annotation has more than one abstract method, the compiler will throw an error.

Example of a Functional Interface

@FunctionalInterface
interface Greeting {
    void sayHello();  // Single abstract method
}

✅ Since there is only one abstract method, this is a valid functional interface.


3. Using Functional Interfaces with Lambda Expressions

A functional interface can be implemented using lambda expressions, making the code cleaner and more readable.

Example: Traditional Implementation (Before Java 8)

public class Main {
    public static void main(String[] args) {
        Greeting greeting = new Greeting() {
            @Override
            public void sayHello() {
                System.out.println("Hello, Functional Interface!");
            }
        };
        greeting.sayHello();
    }
}

Example: Using Lambda Expression (Java 8+)

public class Main {
    public static void main(String[] args) {
        Greeting greeting = () -> System.out.println("Hello, Lambda!");
        greeting.sayHello();
    }
}

Benefits of using lambda expressions:

  • Less code – No need for an anonymous class.
  • Easier to read – Simple and clear syntax.

4. Built-in Functional Interfaces in Java (java.util.function Package)

Java provides several pre-defined functional interfaces in the java.util.function package. Here are some commonly used ones:

Functional InterfaceMethodDescriptionExample
Predicate<T>boolean test(T t)Tests if a condition is metPredicate<Integer> isEven = (n) -> n % 2 == 0;
Function<T, R>R apply(T t)Transforms a valueFunction<String, Integer> length = str -> str.length();
Consumer<T>void accept(T t)Performs an actionConsumer<String> print = str -> System.out.println(str);
Supplier<T>T get()Provides a valueSupplier<Double> random = () -> Math.random();

5. Example Using a Built-in Functional Interface

Using Predicate<T> to Check if a Number is Even

import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<Integer> isEven = (n) -> n % 2 == 0;
        System.out.println(isEven.test(10)); // true
        System.out.println(isEven.test(7));  // false
    }
}

✅ The Predicate<Integer> checks if a number is even, returning true or false.


Lesson Reflection

Think about the following questions:

  1. Why do you think Java introduced functional interfaces when it already had regular interfaces?
  2. How does using lambda expressions with functional interfaces improve code readability and maintainability?
  3. Can you think of real-world applications where built-in functional interfaces like Predicate, Function, or Consumer would be useful?

Examples for functional interfaces

Let’s go through examples for each of the remaining built-in functional interfaces from java.util.function.


1. Function<T, R> – Transforming a Value

The Function<T, R> interface takes an input of type T and returns a value of type R.

Example: Convert a String to Its Length

import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<String, Integer> getLength = str -> str.length();
        
        System.out.println(getLength.apply("Java"));  // Output: 4
        System.out.println(getLength.apply("Functional Interface")); // Output: 21
    }
}

✅ Here, Function<String, Integer> takes a String and returns an Integer (its length).


2. Consumer<T> – Performing an Action (No Return Value)

The Consumer<T> interface accepts a value but does not return anything. It’s useful for operations like printing, logging, or modifying values.

Example: Print a List of Names

import java.util.function.Consumer;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        Consumer<String> printName = name -> System.out.println("Hello, " + name + "!");
        
        List<String> names = List.of("Alice", "Bob", "Charlie");
        names.forEach(printName);
    }
}

✅ Here, Consumer<String> takes a String and prints a greeting message without returning anything.


3. Supplier<T> – Providing a Value (No Input Required)

The Supplier<T> interface does not take any arguments, but returns a value of type T. It’s useful for generating values like random numbers, timestamps, or database connections.

Example: Generate a Random Number

import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) {
        Supplier<Double> randomNumber = () -> Math.random();
        
        System.out.println(randomNumber.get());  // Output: (random number)
        System.out.println(randomNumber.get());  // Output: (random number)
    }
}

✅ Here, Supplier<Double> generates a random number whenever get() is called.

Below are the answers to the reflection questions:

1. Which of these functional interfaces do you find most useful? Why?

It depends on the use case, but Function<T, R> and Predicate<T> are often the most useful in real-world applications.

  • Function<T, R> is valuable when transforming data, such as converting user input, processing API responses, or performing mathematical calculations.
  • Predicate<T> is essential for filtering data, such as checking if a user is authorized, validating input fields, or filtering a list of products based on price.
  • Consumer<T> is useful for performing actions like logging or printing results.
  • Supplier<T> is great when delayed execution is needed, such as fetching configuration values or generating unique IDs.

If I had to pick just one, Function<T, R> would be the most versatile because it allows for data transformation, which is a common operation in programming.


2. Can you think of a scenario where a Supplier<T> might be useful besides generating random numbers?

Yes! Here are a few real-world use cases for Supplier<T>:

Lazy Initialization of Expensive Resources

  • Suppose we have a database connection that is expensive to create. Instead of creating it immediately, we use Supplier<T> to create it only when needed.

Supplier<Connection> dbConnection = () -> {
    System.out.println("Connecting to the database...");
    return DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
};

Fetching System Properties or Configuration Values

  • Imagine an application that needs a value from a configuration file. We can use a Supplier<String> to fetch it on demand.

Supplier<String> configValue = () -> System.getProperty("user.home");
System.out.println("User Home Directory: " + configValue.get());

This ensures the value is not fetched until actually needed, improving efficiency.

Generating Unique Identifiers

  • We can use a Supplier<UUID> to generate unique identifiers for user sessions or database records.

Supplier<UUID> uniqueIdSupplier = UUID::randomUUID;
System.out.println("Generated ID: " + uniqueIdSupplier.get());

Every time we call uniqueIdSupplier.get(), a new UUID is created.

Caching Expensive Computations

  • Suppose an operation (e.g., loading a large file) is expensive, and we want to defer it until needed.

Supplier<String> expensiveComputation = () -> {
    System.out.println("Performing expensive operation...");
    return "Computed Result";
};

The computation only runs when expensiveComputation.get() is called, avoiding unnecessary execution.


next concept: Streams API

Java Sleep