JAVA

Java Functional Interfaces

neal89 2025. 4. 1. 13:13

With the introduction of Lambda Expressions in Java 8, Java began supporting a more functional programming style. At the heart of this is the Functional Interface—a special type of interface that allows us to pass behavior as data.

In this post, we’ll break down what functional interfaces are, explore the key types provided by Java, and see how to use them effectively with practical examples.


✅ What Is a Functional Interface?

A Functional Interface is an interface that contains exactly one abstract method.

  • Also called a SAM interface (Single Abstract Method).
  • Use the @FunctionalInterface annotation to let the compiler verify it.
  • Lambda expressions are used to create instances of functional interfaces in a concise way.
@FunctionalInterface
interface MyFunction {
    int apply(int x, int y);
}

🔄 Functional Interface + Lambda = ❤️

A lambda expression is a shorthand for implementing a functional interface.

MyFunction add = (a, b) -> a + b;
System.out.println(add.apply(3, 4)); // Output: 7

📚 Built-in Functional Interfaces in Java

Java provides a rich set of pre-defined functional interfaces under the java.util.function package.

Interface Method Signature Input(s) Output Purpose

Function<T, R> R apply(T t) 1 1 Transformation, mapping
Consumer<T> void accept(T t) 1 None Side effects (print, save)
Supplier<T> T get() None 1 Generate/provide value
Predicate<T> boolean test(T t) 1 boolean Conditional checks, filtering
UnaryOperator<T> T apply(T t) 1 1 (same) Single-type transformation
BinaryOperator<T> T apply(T t1, T t2) 2 1 (same) Reduce, binary ops

🔍 Usage Examples

🔸 Function<T, R>

Function<String, Integer> lengthFunc = s -> s.length();
System.out.println(lengthFunc.apply("Hello")); // 5

🔸 Consumer<T>

Consumer<String> printer = s -> System.out.println("Print: " + s);
printer.accept("Lambda");

🔸 Supplier<T>

Supplier<Double> random = () -> Math.random();
System.out.println(random.get());

🔸 Predicate<T>

Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // true

🔸 UnaryOperator<T>

UnaryOperator<String> toUpper = s -> s.toUpperCase();
System.out.println(toUpper.apply("java")); // "JAVA"

🔸 BinaryOperator<T>

BinaryOperator<Integer> sum = (a, b) -> a + b;
System.out.println(sum.apply(2, 3)); // 5

🧪 Are Lambdas or Anonymous Classes Instances?

Yes! Both lambdas and anonymous classes create instances at runtime:

  • Anonymous classes → compiled into their own .class file (e.g., Outer$1.class)
  • Lambdas → handled via invokedynamic, more lightweight and efficient

⚔️ Anonymous Class vs Lambda — What’s the Difference?

Feature Anonymous Class Lambda Expression

this keyword Refers to the anonymous class Refers to the enclosing class
Scope Creates a new inner scope Shares outer scope
Variable shadowing Allowed Not allowed
Instance creation Creates a new class file Uses dynamic call site

💡 this Reference Difference

public class ThisExample {
    String name = "Outer";

    void show() {
        Runnable anon = new Runnable() {
            String name = "InnerAnon";
            public void run() {
                System.out.println("Anon this.name = " + this.name); // InnerAnon
            }
        };

        Runnable lambda = () -> {
            System.out.println("Lambda this.name = " + this.name); // Outer
        };

        anon.run();
        lambda.run();
    }

    public static void main(String[] args) {
        new ThisExample().show();
    }
}

Output:

Anon this.name = InnerAnon
Lambda this.name = Outer
  • Anonymous class: this refers to the anonymous object
  • Lambda: this refers to the outer class instance

🧠 Primitive Type Support

Java’s generics don’t support primitives like int, so Java offers these interfaces:

Interface Description

IntFunction<R> int → R
ToIntFunction<T> T → int
IntPredicate int → boolean
IntSupplier, etc. Work with primitive values
IntFunction<String> f = x -> "Value: " + x;
System.out.println(f.apply(100)); // Value: 100

📌 Summary: Which Interface Should I Use?

Use this table to decide:

Use Case  Interface to Use
Transform value Function<T, R>
Just process input Consumer<T>
Generate value Supplier<T>
Conditional check Predicate<T>
One input, same return type UnaryOperator<T>
Two inputs, same return BinaryOperator<T>

 

Prefer built-in interfaces over custom ones. They're standard, readable, and interoperable.