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
Create a method that takes a list of strings, filters them using a Predicate, and transforms them using a Function. Return the resulting list.
Solution
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
public class Pipeline {
public static <T, R> List<R> pipeline(List<T> input, Predicate<T> filter, Function<T, R> transform) {
return input.stream()
.filter(filter)
.map(transform)
.collect(Collectors.toList());
}
}Create a Validator functional interface with validate(), and() default methods. Then create validators for checking if a string is not empty, has minimum length, and contains only alphanumeric characters.
Solution
@FunctionalInterface
interface Validator<T> {
boolean validate(T input);
default Validator<T> and(Validator<T> other) {
return input -> this.validate(input) && other.validate(input);
}
}
public class ValidatorDemo {
public static void main(String[] args) {
Validator<String> notEmpty = s -> s != null && !s.isEmpty();
Validator<String> minLength = s -> s != null && s.length() >= 6;
Validator<String> alphanumeric = s -> s != null && s.matches("[a-zA-Z0-9]+");
Validator<String> combined = notEmpty.and(minLength).and(alphanumeric);
System.out.println("Valid: " + combined.validate("hello123")); // true
System.out.println("Invalid: " + combined.validate("hi")); // false
}
}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()
Question
What is Java Functional Interfaces?
Click to reveal answer
Answer
Java Functional Interfaces is a key concept in Java programming.
Question
When to use Java Functional Interfaces?
Click to reveal answer
Answer
Use Java Functional Interfaces when building production systems that require reliability, scalability, and maintainability.
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