Functional Interfaces in Java
An interface with exactly one abstract method can be implemented by a lambda, and the standard library supplies the ones you need most.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
Definition
A functional interface has exactly one abstract method. That single method is the target a lambda or method reference implements.
@FunctionalInterface
public interface Validator {
boolean isValid(String value);
}Validator notBlank = value -> value != null && !value.isBlank();
Validator isEmail = value -> value.contains("@");
System.out.println(notBlank.isValid(" ")); // falseThe annotation is optional but valuable: it makes the intent explicit and turns adding a second abstract method into a compile error.
Default and static methods do not count
@FunctionalInterface
public interface Validator {
boolean isValid(String value); // the one abstract method
default Validator and(Validator other) { // default, allowed
return value -> isValid(value) && other.isValid(value);
}
static Validator alwaysValid() { // static, allowed
return value -> true;
}
}Validator strict = notBlank.and(isEmail);
System.out.println(strict.isValid("a@b.com")); // trueMethods inherited from Object, such as equals, also do not count towards the total.
The built in interfaces
| Interface | Method | Takes | Returns | Use for |
|---|---|---|---|---|
Predicate<T> | test | T | boolean | A condition |
Function<T,R> | apply | T | R | A transformation |
Consumer<T> | accept | T | nothing | A side effect |
Supplier<T> | get | nothing | T | Producing a value |
UnaryOperator<T> | apply | T | T | Same type transformation |
BinaryOperator<T> | apply | T, T | T | Combining two values |
BiFunction<T,U,R> | apply | T, U | R | Two inputs |
BiPredicate<T,U> | test | T, U | boolean | A two argument condition |
BiConsumer<T,U> | accept | T, U | nothing | Map iteration |
Runnable | run | nothing | nothing | A task |
Callable<V> | call | nothing | V, may throw | A task with a result |
Predicate
Predicate<String> notBlank = value -> !value.isBlank();
Predicate<String> isLong = value -> value.length() > 10;
System.out.println(notBlank.and(isLong).test("a long enough value")); // true
System.out.println(notBlank.or(isLong).test("hi")); // true
System.out.println(notBlank.negate().test("")); // true
System.out.println(Predicate.not(String::isBlank).test("x")); // trueFunction
Function<String, Integer> length = String::length;
Function<Integer, String> describe = n -> n + " characters";
System.out.println(length.andThen(describe).apply("java")); // 4 characters
System.out.println(describe.compose(length).apply("java")); // the same
System.out.println(Function.<String>identity().apply("x")); // xandThen runs this function first; compose runs the argument first. Reading them out loud settles the order every time.
Consumer and Supplier
Consumer<String> print = System.out::println;
Consumer<String> log = value -> logger.info(value);
print.andThen(log).accept("saved"); // both run, in order
Supplier<List<String>> empty = ArrayList::new;
Supplier<Double> random = Math::random;A Supplier is how laziness is expressed. Optional.orElseGet(supplier) and Map.computeIfAbsent both take one so the value is produced only when it is actually needed.
Operators
UnaryOperator<String> trim = String::strip;
BinaryOperator<Integer> add = Integer::sum;
List<String> values = new ArrayList<>(List.of(" a ", " b "));
values.replaceAll(trim); // takes a UnaryOperator
int total = Stream.of(1, 2, 3).reduce(0, add); // takes a BinaryOperatorPrimitive versions
IntPredicate isEven = n -> n % 2 == 0; // no Integer boxing
IntFunction<String> describe = n -> "value " + n;
ToIntFunction<String> parse = Integer::parseInt;
IntUnaryOperator doubled = n -> n * 2;
IntSupplier constant = () -> 7;There is a primitive variant for int, long and double across the whole family. In a loop over millions of values these avoid a boxing allocation on every step.
Writing your own
@FunctionalInterface
public interface NoteTransformer {
Note transform(Note note);
default NoteTransformer then(NoteTransformer next) {
return note -> next.transform(transform(note));
}
}NoteTransformer pipeline = ((NoteTransformer) Note::stripHtml)
.then(Note::normaliseTitle)
.then(Note::addSummary);Define your own only when the built in ones do not express the domain. NoteTransformer reads better than UnaryOperator<Note> at a call site, and it can carry domain specific default methods.
Common mistakes
- Adding a second abstract method and breaking every lambda that used the interface.
- Creating a custom interface where
FunctionorPredicatealready fits. - Using boxed interfaces in hot loops instead of the primitive variants.
- Expecting the built in interfaces to allow checked exceptions. They do not.
- Confusing
andThenwithcompose.
Best practices
- Annotate custom functional interfaces with
@FunctionalInterface. - Prefer the standard interfaces so your API is instantly familiar.
- Define your own when the domain name genuinely adds clarity.
- Use the primitive variants where performance matters.
- Compose with
and,or,negateandandThenrather than nesting lambdas by hand.
Practice
- Write a
Predicate<String>that accepts values between 3 and 20 characters, built from two composed predicates. - Why does adding a second abstract method break existing lambdas?
- Chain three
Functionobjects to trim, lower case and then measure a string. - Which built in interface fits: producing a default configuration, logging a value, converting a note to JSON, testing whether a note is published?
- Rewrite a boxed
Predicate<Integer>as anIntPredicateand explain the gain.
Conclusion
A functional interface is one abstract method with a name. Learn the six core shapes, use the primitive variants when it matters, compose rather than nest, and define your own only when the domain deserves its own vocabulary.