Predicate
Predicate
Predicate<T> is a functional interface that takes a value of type T and returns a boolean. It is used for testing conditions.
Method: boolean test(T t)
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.*;
public class PredicateDemo {
public static void main(String[] args) {
// Basic predicate
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();
System.out.println("Empty? " + isEmpty.test("")); // true
System.out.println("Not empty? " + isNotEmpty.test("hello")); // true
// Predicate chaining
Predicate<String> startsWithH = s -> s.startsWith("H");
Predicate<String> hasLength5 = s -> s.length() == 5;
Predicate<String> startsWithHAndLength5 = startsWithH.and(hasLength5);
Predicate<String> startsWithHOrLength5 = startsWithH.or(hasLength5);
System.out.println("Hello matches both: " + startsWithHAndLength5.test("Hello")); // true
System.out.println("Hi matches either: " + startsWithHOrLength5.test("Hi")); // true
// Using predicates with collections
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");
// Filter with predicate
List<String> longNames = names.stream()
.filter(s -> s.length() > 4)
.collect(Collectors.toList());
System.out.println("Long names: " + longNames); // [Alice, Charlie, David]
// Compose predicates
Predicate<String> isShort = s -> s.length() <= 3;
Predicate<String> startsWithB = s -> s.startsWith("B");
List<String> shortOrStartsWithB = names.stream()
.filter(isShort.or(startsWithB))
.collect(Collectors.toList());
System.out.println("Short or B: " + shortOrStartsWithB); // [Bob, Eve]
// Practical: validation
Predicate<String> validEmail = s -> s != null && s.contains("@") && s.contains(".");
Predicate<Integer> validAge = age -> age >= 0 && age <= 150;
Predicate<String> validPassword = s -> s != null && s.length() >= 8
&& s.matches(".*[A-Z].*") && s.matches(".*[0-9].*");
System.out.println("Valid email: " + validEmail.test("user@example.com")); // true
System.out.println("Valid age: " + validAge.test(25)); // true
System.out.println("Valid password: " + validPassword.test("Pass1234")); // true
}
}
Predicate methods:
test(T t)— evaluate the predicateand(Predicate)— logical ANDor(Predicate)— logical ORnegate()— logical NOTisEqual(Object)— equality predicate
Function
Function<T, R>
Function<T, R> takes a value of type T and returns a value of type R. It is used for transformations.
Method: R apply(T t)
import java.util.*;
import java.util.function.Function;
import java.util.stream.*;
public class FunctionDemo {
public static void main(String[] args) {
// Basic function
Function<String, Integer> toLength = String::length;
Function<String, String> toUpper = String::toUpperCase;
Function<String, String> toLower = String::toLowerCase;
System.out.println("Length of Hello: " + toLength.apply("Hello")); // 5
System.out.println("Upper: " + toUpper.apply("hello")); // HELLO
// Function chaining
Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, String> process = trim.andThen(upper);
System.out.println("Processed: " + process.apply(" hello ")); // HELLO
// compose vs andThen
// compose: apply THIS function AFTER the argument function
// andThen: apply THIS function BEFORE the argument function
Function<Integer, Integer> times2 = x -> x * 2;
Function<Integer, Integer> plus3 = x -> x + 3;
System.out.println("compose: " + times2.compose(plus3).apply(5)); // (5+3)*2 = 16
System.out.println("andThen: " + times2.andThen(plus3).apply(5)); // (5*2)+3 = 13
// Using functions with streams
List<String> names = Arrays.asList("alice", "bob", "charlie");
List<String> processed = names.stream()
.map(s -> s.substring(0, 1).toUpperCase() + s.substring(1))
.collect(Collectors.toList());
System.out.println("Processed: " + processed); // [Alice, Bob, Charlie]
// Practical: parsing
Function<String, Integer> safeParse = s -> {
try {
return Integer.parseInt(s.trim());
} catch (NumberFormatException e) {
return 0;
}
};
System.out.println("Parsed: " + safeParse.apply("42")); // 42
System.out.println("Bad parse: " + safeParse.apply("abc")); // 0
// identity function
Function<String, String> identity = Function.identity();
System.out.println("Identity: " + identity.apply("hello")); // hello
}
}
Function methods:
apply(T t)— apply the functionandThen(Function)— apply after this functioncompose(Function)— apply before this functionidentity()— returns the input unchanged
Consumer
Consumer
Consumer<T> takes a value of type T and returns nothing. It is used for side effects (printing, logging, modifying state).
Method: void accept(T t)
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.*;
public class ConsumerDemo {
public static void main(String[] args) {
// Basic consumer
Consumer<String> print = System.out::println;
Consumer<String> printUpper = s -> System.out.println(s.toUpperCase());
print.accept("Hello"); // Hello
printUpper.accept("Hello"); // HELLO
// Consumer chaining
Consumer<String> log = s -> System.out.println("LOG: " + s);
Consumer<String> store = s -> System.out.println("STORE: " + s);
Consumer<String> logAndStore = log.andThen(store);
logAndStore.accept("data");
// LOG: data
// STORE: data
// Using consumers with forEach
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println("Hello, " + name + "!"));
// Practical: building a report
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Consumer<List<Integer>> printStats = list -> {
int sum = list.stream().mapToInt(Integer::intValue).sum();
double avg = (double) sum / list.size();
System.out.println("Count: " + list.size());
System.out.println("Sum: " + sum);
System.out.println("Average: " + avg);
System.out.println("Min: " + list.stream().min(Integer::compareTo).orElse(0));
System.out.println("Max: " + list.stream().max(Integer::compareTo).orElse(0));
};
printStats.accept(numbers);
// Practical: modifying objects
class Person {
String name;
int age;
Person(String name, int age) { this.name = name; this.age = age; }
@Override
public String toString() { return name + "(" + age + ")"; }
}
Consumer<Person> birthday = p -> p.age++;
Consumer<Person> printPerson = p -> System.out.println(p);
Person alice = new Person("Alice", 30);
birthday.andThen(printPerson).accept(alice); // Alice(31)
}
}
Consumer methods:
accept(T t)— perform the actionandThen(Consumer)— chain another consumer after this one
Common patterns:
list.forEach(System.out::println)— print each elementlist.forEach(consumer.andThen(other))— chain side effectsoptional.ifPresent(consumer)— action if value present
Supplier
Supplier
Supplier<T> takes no arguments and returns a value of type T. It is used for producing values, lazy initialization, and factory patterns.
Method: T get()
import java.util.*;
import java.util.function.Supplier;
public class SupplierDemo {
public static void main(String[] args) {
// Basic supplier
Supplier<String> greeting = () -> "Hello, World!";
Supplier<Double> random = Math::random;
Supplier<List<String>> listFactory = ArrayList::new;
System.out.println(greeting.get()); // Hello, World!
System.out.println("Random: " + random.get());
List<String> newList = listFactory.get();
// Supplier for lazy evaluation
Supplier<String> expensiveComputation = () -> {
System.out.println("Computing...");
return "result";
};
// Only computed when get() is called
System.out.println("Before get()");
String result = expensiveComputation.get(); // computes now
System.out.println("After get(): " + result);
// Practical: lazy initialization
class Database {
private Supplier<Connection> connectionFactory;
private Connection connection;
Database(Supplier<Connection> factory) {
this.connectionFactory = factory;
}
Connection getConnection() {
if (connection == null) {
connection = connectionFactory.get(); // lazy init
}
return connection;
}
}
// Practical: random data generation
Supplier<String> randomName = () -> {
String[] names = {"Alice", "Bob", "Charlie", "David"};
return names[(int) (Math.random() * names.length)];
};
System.out.println("Random name: " + randomName.get());
// Practical: factory pattern
Supplier<Map<String, Integer>> hashMapFactory = HashMap::new;
Supplier<Map<String, Integer>> treeMapFactory = TreeMap::new;
Map<String, Integer> hashMap = hashMapFactory.get();
Map<String, Integer> treeMap = treeMapFactory.get();
System.out.println("HashMap class: " + hashMap.getClass());
System.out.println("TreeMap class: " + treeMap.getClass());
}
// Connection placeholder
static class Connection {
@Override
public String toString() { return "Connection"; }
}
}
Supplier methods:
get()— produce a value
Common patterns:
- Lazy evaluation: compute only when needed
- Factory pattern: create new instances
- Default values:
Optional.orElseGet(supplier)
Custom Functional Interfaces
Custom Functional Interfaces
You can create your own functional interfaces using the @FunctionalInterface annotation.
@FunctionalInterface
public interface Transformer<T> {
T transform(T input);
}
@FunctionalInterface
public interface TriFunction<A, B, C, R> {
R apply(A a, B b, C c);
}
@FunctionalInterface
public interface Validator<T> {
boolean validate(T input);
default Validator<T> and(Validator<T> other) {
return input -> this.validate(input) && other.validate(input);
}
default Validator<T> or(Validator<T> other) {
return input -> this.validate(input) || other.validate(input);
}
}
// Usage
import java.util.*;
public class CustomFunctionalInterfaceDemo {
public static void main(String[] args) {
// Transformer
Transformer<String> shout = s -> s.toUpperCase() + "!";
Transformer<Integer> doubleIt = n -> n * 2;
System.out.println(shout.transform("hello")); // HELLO!
System.out.println(doubleIt.transform(5)); // 10
// TriFunction
TriFunction<Integer, Integer, Integer, Integer> maxOfThree =
(a, b, c) -> Math.max(a, Math.max(b, c));
System.out.println(maxOfThree.apply(1, 2, 3)); // 3
// Validator with chaining
Validator<String> notEmpty = s -> s != null && !s.isEmpty();
Validator<String> hasAtLeast8Chars = s -> s != null && s.length() >= 8;
Validator<String> hasUpperCase = s -> s != null && s.matches(".*[A-Z].*");
Validator<String> passwordValidator = notEmpty
.and(hasAtLeast8Chars)
.and(hasUpperCase);
System.out.println("Valid: " + passwordValidator.validate("Pass1234")); // true
System.out.println("Invalid: " + passwordValidator.validate("pass")); // false
// Using custom interface with method reference
List<String> words = Arrays.asList("hello", "world", "java");
words.forEach(System.out::println);
// Practical: callback interface
@FunctionalInterface
interface Callback<T> {
void onComplete(T result);
default void onError(Throwable t) {
System.err.println("Error: " + t.getMessage());
}
}
Callback<String> callback = new Callback<String>() {
@Override
public void onComplete(String result) {
System.out.println("Result: " + result);
}
};
callback.onComplete("done");
callback.onError(new RuntimeException("oops"));
}
}
Key points:
@FunctionalInterfaceensures the interface has exactly one abstract method- It can have default and static methods
- Custom interfaces work with lambdas and method references
- Design for the specific use case (not generic like Predicate/Function)
Practice Problems
Given an array of integers nums, sort the array in ascending order.
Example:
Input: nums = [5, 2, 3, 1]
Output: [1, 2, 3, 5]
Sort in ascending order
Find the kth largest element in an unsorted array.
Example:
Input: nums = [3, 2, 1, 5, 6, 4], k = 2
Output: 5
The 2nd largest element is 5
Quiz
1. What is the method signature of Predicate<T>?
2. What is the difference between Function.compose() and Function.andThen()?
3. Which functional interface is best for producing a value without arguments?
4. What does the @FunctionalInterface annotation do?
Flashcards
Question
What are the 4 core functional interfaces in Java?
Click to reveal answer
Answer
Predicate<T> (boolean test(T)), Function<T,R> (R apply(T)), Consumer<T> (void accept(T)), Supplier<T> (T get()). Each serves a different purpose: conditions, transformations, side effects, and production.
Question
When would you use Consumer vs Function?
Click to reveal answer
Answer
Consumer for side effects that don't return a value (printing, logging, modifying state). Function for transformations that produce a new value (parsing, converting, mapping).
Question
How do you chain Predicates?
Click to reveal answer
Answer
Use .and() for AND, .or() for OR, .negate() for NOT. Example: predicate1.and(predicate2).or(predicate3).negate()
Revision Notes
Key Takeaways
- 1. Predicate tests conditions (boolean return)
- 2. Function transforms values (produces new value)
- 3. Consumer performs side effects (no return)
- 4. Supplier produces values (no arguments)
- 5. All support chaining with default methods
Interview Tips
- • Explain each core functional interface and when to use each
- • Demonstrate chaining: Predicate.and/or, Function.andThen/compose
- • Know the difference between compose (applies before) and andThen (applies after)
- • Be ready to create custom functional interfaces for specific use cases
Cheat Sheet
Functional Interfaces Cheat Sheet
Core Interfaces
- Predicate
: boolean test(T t) - Function<T,R>: R apply(T t)
- Consumer
: void accept(T t) - Supplier
: T get()
Chaining
- Predicate: and(), or(), negate()
- Function: andThen(), compose()
- Consumer: andThen()
Common Methods
- Predicate.isEqual(obj)
- Function.identity()
- Consumer.andThen(other)
Custom
- @FunctionalInterface
- One abstract method
- Can have default/static methods