Index

Working with Functional Interfaces

Java Functional Programming

3.1 Creating Custom Functional Interfaces

While Java 8 provides many built-in functional interfaces in the java.util.function package (like Function, Predicate, Consumer, and Supplier), there are times when these don’t match your specific use case. In such cases, you can create your own custom functional interfaces tailored to your needs.

A functional interface is simply an interface with a single abstract method (SAM). These interfaces can be used as targets for lambda expressions and method references, enabling clean and expressive code.

The @FunctionalInterface Annotation

Java provides the @FunctionalInterface annotation to explicitly mark an interface as functional. This annotation is optional but recommended—it tells the compiler to enforce that the interface contains only one abstract method. If you accidentally add a second abstract method, the compiler will raise an error, helping prevent mistakes.

Defining a Custom Functional Interface

Let’s create a functional interface for a custom operation that takes two integers and returns a string:

@FunctionalInterface
interface IntToStringOperation {
    String apply(int a, int b);

    // Optional: default method
    default void printExample() {
        System.out.println("This interface converts two ints into a string.");
    }

    // Optional: static method
    static void showInfo() {
        System.out.println("IntToStringOperation is a custom functional interface.");
    }
}

This interface has one abstract method, apply, and optional default and static methods for extended functionality.

Example 1: Using the Custom Interface with a Lambda

public class CustomFunctionalInterfaceDemo {
    public static void main(String[] args) {
        IntToStringOperation concat = (a, b) -> "Combined: " + a + b;
        System.out.println(concat.apply(10, 20)); // Output: Combined: 1020

        concat.printExample();
        IntToStringOperation.showInfo();
    }
}
Click to view full runnable Code

@FunctionalInterface
interface IntToStringOperation {
    String apply(int a, int b);

    // Optional: default method
    default void printExample() {
        System.out.println("This interface converts two ints into a string.");
    }

    // Optional: static method
    static void showInfo() {
        System.out.println("IntToStringOperation is a custom functional interface.");
    }
}

public class CustomFunctionalInterfaceDemo {
    public static void main(String[] args) {
        IntToStringOperation concat = (a, b) -> "Combined: " + a + b;
        System.out.println(concat.apply(10, 20)); // Output: Combined: 1020

        concat.printExample(); // Default method
        IntToStringOperation.showInfo(); // Static method
    }
}

Example 2: A No-Argument Functional Interface

You can also define interfaces with no parameters:

@FunctionalInterface
interface MessageProvider {
    String getMessage();
}

public class MessageExample {
    public static void main(String[] args) {
        MessageProvider provider = () -> "Hello from a custom interface!";
        System.out.println(provider.getMessage());
    }
}

Why Use Custom Functional Interfaces?

Creating your own functional interfaces allows for expressive, type-safe, and reusable functional constructs tailored to your application’s needs.

Index

3.2 Default and Static Methods in Interfaces

Prior to Java 8, interfaces could only contain abstract methods, meaning every implementing class had to provide the method’s behavior. Java 8 introduced default and static methods in interfaces, bringing more flexibility and power—especially in the context of functional programming.

Default Methods

A default method provides a method implementation directly within the interface using the default keyword. This allows interfaces to evolve without breaking existing implementations. Default methods are especially useful in functional interfaces, where they can offer reusable behaviors or compose functions.

Example 1: Default Method for Composition

@FunctionalInterface
interface Formatter {
    String format(String input);

    // Compose uppercase formatting with prefix
    default Formatter withPrefix(String prefix) {
        return (s) -> prefix + format(s);
    }
}

public class DefaultMethodExample {
    public static void main(String[] args) {
        Formatter upperCase = s -> s.toUpperCase();
        Formatter withGreeting = upperCase.withPrefix("Hello, ");

        System.out.println(withGreeting.format("world")); // Output: Hello, WORLD
    }
}

In this example, withPrefix is a default method that returns a new composed Formatter.

Static Methods

Static methods in interfaces are utility methods related to the interface’s behavior. These can be called without an instance and are often used as factory or helper methods.

Example 2: Static Factory Method in Functional Interface

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

    static MathOperation multiply() {
        return (a, b) -> a * b;
    }
}

public class StaticMethodExample {
    public static void main(String[] args) {
        MathOperation op = MathOperation.multiply();
        System.out.println(op.operate(4, 5)); // Output: 20
    }
}

Combining Both

Default and static methods work well together. You can create reusable, composable, and testable patterns using both.

Example 3: Combining Defaults and Statics

@FunctionalInterface
interface Validator {
    boolean isValid(String value);

    default Validator and(Validator other) {
        return s -> this.isValid(s) && other.isValid(s);
    }

    static Validator notEmpty() {
        return s -> s != null && !s.isEmpty();
    }
}

public class ValidatorExample {
    public static void main(String[] args) {
        Validator validator = Validator.notEmpty().and(s -> s.length() >= 3);
        System.out.println(validator.isValid("abc")); // Output: true
        System.out.println(validator.isValid(""));    // Output: false
    }
}

Summary: Default methods support behavior sharing and composition without forcing all implementations to override them, while static methods offer reusable helpers. Together, they enrich functional interfaces with utility, composability, and backwards compatibility.

Index

3.3 Composing Functions and Predicates

One of the key strengths of functional programming is the ability to compose small, reusable functions to build more complex behavior. Java’s functional interfaces like Function and Predicate provide built-in methods such as andThen, compose, and, or, and negate to support this composition. These methods allow chaining and combining logic in a clear and expressive way.

Function Composition

The Function<T, R> interface provides two important methods:

This is useful for building data transformation pipelines.

Example 1: Chaining Functions

import java.util.function.Function;

public class FunctionCompositionExample {
    public static void main(String[] args) {
        Function<String, String> trim = String::trim;
        Function<String, String> toUpper = String::toUpperCase;
        Function<String, Integer> length = String::length;

        // Compose: trim -> toUpper -> length
        Function<String, Integer> composed = trim.andThen(toUpper).andThen(length);

        System.out.println(composed.apply("  hello  ")); // Output: 5
    }
}

Here, the input string is trimmed, converted to uppercase, and its length is calculated—all using function composition.

Predicate Composition

The Predicate<T> interface includes:

These allow building complex conditions from simpler tests.

Example 2: Combining Predicates

import java.util.List;
import java.util.function.Predicate;

public class PredicateCombinationExample {
    public static void main(String[] args) {
        List<String> names = List.of("Alice", "", null, "Bob", "  ", "Charlie");

        Predicate<String> notNull = s -> s != null;
        Predicate<String> notEmpty = s -> !s.isEmpty();
        Predicate<String> notBlank = s -> !s.trim().isEmpty();

        // Combined predicate to filter valid names
        Predicate<String> isValid = notNull.and(notEmpty).and(notBlank);

        names.stream()
             .filter(isValid)
             .forEach(System.out::println); // Output: Alice, Bob, Charlie
    }
}

This example filters a list to remove null, empty, or blank strings using predicate composition.

Example 3: Function Predicate in a Real Use Case

import java.util.function.Function;
import java.util.function.Predicate;

public class EmailValidation {
    public static void main(String[] args) {
        Function<String, String> normalize = email -> email.toLowerCase().trim();
        Predicate<String> containsAt = email -> email.contains("@");
        Predicate<String> validDomain = email -> email.endsWith(".com");

        String rawEmail = "  USER@Example.COM ";

        String cleaned = normalize.apply(rawEmail);
        boolean isValid = containsAt.and(validDomain).test(cleaned);

        System.out.println("Normalized: " + cleaned);     // Output: user@example.com
        System.out.println("Valid email: " + isValid);    // Output: true
    }
}

Why Composition Matters

Function and predicate composition allows you to:

By chaining behavior, your code becomes declarative, expressive, and easy to test—hallmarks of good functional programming in Java.

Index

3.4 Example: Filtering and Transforming Collections

Functional programming in Java simplifies operations on collections by allowing us to declaratively express filtering, mapping, and processing using lambda expressions and functional interfaces. In this example, we'll work with a list of Person objects. We'll filter out only adults (age ≥ 18), and then transform each Person into a String greeting message.

We’ll use:

Full Example

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

public class CollectionFilterTransform {
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
            new Person("Alice", 22),
            new Person("Bob", 15),
            new Person("Charlie", 30),
            new Person("Daisy", 17)
        );

        // Predicate to filter adults
        Predicate<Person> isAdult = p -> p.getAge() >= 18;

        // Function to convert Person to greeting message
        Function<Person, String> toGreeting = p -> "Hello, " + p.getName() + "!";

        // Process: filter adults, transform to greeting strings
        List<String> greetings = people.stream()
            .filter(isAdult)
            .map(toGreeting)
            .collect(Collectors.toList());

        // Output the result
        greetings.forEach(System.out::println);
    }
}

Expected Output

Hello, Alice!
Hello, Charlie!

Explanation

Possible Variations

This example highlights how Java’s functional programming model transforms verbose loops into clean, readable, and composable operations using predicates, functions, and the stream API.

Index