Master Java Functional Interfaces: Practical Guide to Comparator, Predicate, and Function
This article explains Java's functional interfaces—Predicate, Function, Consumer, Supplier, and Comparator—showing their method signatures, how to compose them, custom interface creation, common pitfalls, and concise code examples for everyday use.
Functional Interfaces Overview
Java 8 introduced lambda expressions; a functional interface is an interface that contains exactly one abstract method and can be instantiated with a lambda expression.
Core Interfaces in java.util.function
Predicate<T>– method boolean test(T t) – evaluates a condition and returns a boolean. Function<T,R> – method R apply(T t) – transforms a value of type T to type R. Consumer<T> – method void accept(T t) – consumes a value without returning a result. Supplier<T> – method T get() – provides a value without any input parameters.
1. Predicate – Conditional Checks
import java.util.function.Predicate;
import java.util.List;
import java.util.stream.Collectors;
public class PredicateDemo {
public static void main(String[] args) {
// Define predicate conditions
Predicate<String> isLong = s -> s.length() > 5;
Predicate<String> startWithJ = s -> s.startsWith("J");
System.out.println(isLong.test("Java")); // false
System.out.println(isLong.test("JavaScript")); // true
// Combine predicates
Predicate<String> combined = isLong.and(startWithJ);
System.out.println(combined.test("JavaScript")); // true (long and starts with J)
System.out.println(combined.test("Python")); // false (does not start with J)
Predicate<String> notLong = isLong.negate();
System.out.println(notLong.test("Go")); // true (not long)
// Real use case: filter a collection
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> greaterThan5 = n -> n > 5;
List<Integer> result = numbers.stream()
.filter(isEven.and(greaterThan5))
.collect(Collectors.toList());
System.out.println(result); // [6, 8, 10]
}
}2. Function – Data Transformation
import java.util.function.Function;
public class FunctionDemo {
public static void main(String[] args) {
// String to integer
Function<String, Integer> parseInt = Integer::parseInt;
System.out.println(parseInt.apply("42")); // 42
// Integer to formatted string
Function<Integer, String> intToStr = n -> "Number: " + n;
System.out.println(intToStr.apply(100)); // Number: 100
// andThen – forward composition (f then g)
Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, String> pipeline = trim.andThen(upper);
System.out.println(pipeline.apply(" hello world ")); // HELLO WORLD
// compose – reverse composition (g then f)
Function<Integer, Integer> times2 = x -> x * 2;
Function<Integer, Integer> plus3 = x -> x + 3;
System.out.println(times2.compose(plus3).apply(5)); // (5+3)*2 = 16
System.out.println(times2.andThen(plus3).apply(5)); // 5*2+3 = 13
}
}3. Consumer – Consuming Data
import java.util.function.Consumer;
import java.util.List;
public class ConsumerDemo {
public static void main(String[] args) {
Consumer<String> print = System.out::println;
Consumer<String> printUpper = s -> System.out.println(s.toUpperCase());
print.accept("hello"); // hello
// andThen – sequential execution of Consumers
Consumer<String> both = print.andThen(printUpper);
both.accept("world"); // prints "world" then "WORLD"
// Real use case: forEach on a collection
List<String> names = List.of("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println("Hello, " + name));
names.forEach(System.out::println);
}
}4. Supplier – Providing Data
import java.util.function.Supplier;
import java.util.Random;
public class SupplierDemo {
public static void main(String[] args) {
// No arguments, lazy value provider
Supplier<String> greeting = () -> "Hello, World!";
System.out.println(greeting.get()); // Hello, World!
// Random integer supplier
Supplier<Integer> randomInt = () -> new Random().nextInt(100);
System.out.println(randomInt.get()); // e.g., 42
// Object factory using method reference
Supplier<java.util.ArrayList<String>> listFactory = java.util.ArrayList::new;
java.util.ArrayList<String> list = listFactory.get();
list.add("Java");
System.out.println(list); // [Java]
// Lazy fallback with Optional.orElseGet
String value = java.util.Optional.<String>empty()
.orElseGet(() -> "default value (lazy)");
System.out.println(value);
}
}5. Comparator – Multi‑Field Sorting
import java.util.*;
public class ComparatorDemo {
record Person(String name, int age, double salary) {}
public static void main(String[] args) {
List<Person> people = new ArrayList<>(List.of(
new Person("Charlie", 30, 15000),
new Person("Alice", 25, 20000),
new Person("Bob", 25, 18000)
));
// Sort by age ascending
people.sort(Comparator.comparingInt(Person::age));
people.forEach(p -> System.out.println(p.name() + " " + p.age()));
// Sort by age ascending, then salary descending when ages are equal
people.sort(
Comparator.comparingInt(Person::age)
.thenComparing(Comparator.comparingDouble(Person::salary).reversed())
);
people.forEach(p -> System.out.printf("%s age=%d salary=%.0f%n",
p.name(), p.age(), p.salary()));
}
}6. Defining a Custom Functional Interface
// Optional annotation; recommended for documentation
@FunctionalInterface
public interface StringProcessor {
String process(String input);
// Default method does not affect functional‑interface status
default StringProcessor andThen(StringProcessor after) {
return input -> after.process(this.process(input));
}
}
// Usage example
StringProcessor trim = String::trim;
StringProcessor upper = String::toUpperCase;
StringProcessor pipeline = trim.andThen(upper);
System.out.println(pipeline.process(" hello ")); // HELLO7. Common Pitfalls
Lambda vs. method reference – s -> s.toUpperCase() is equivalent to String::toUpperCase; the method reference is more concise.
Predicate composition – methods and, or, negate return a new Predicate; the original instance remains unchanged.
Consumer andThen – executes both Consumers sequentially; there is no short‑circuiting.
Functional‑interface type inference – the target type is inferred from the surrounding context, allowing the same lambda signature to be assigned to different functional interfaces.
8. Summary of Functional Interfaces
Predicate → test() – condition, combinable with and/or/negate
Function → apply() – data transformation, chainable with andThen/compose
Consumer → accept() – consumes data, commonly used with forEach
Supplier → get() – provides data, useful for lazy loading via orElseGet
Comparator → sort() – multi‑field ordering with comparingXxx + thenComparingSigned-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
