Java Functional Interfaces
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.