Functional Interfaces & Method References

15 questions found

What is a functional interface, and what are the four core functional interfaces in java.util.function (Function, Predicate, Consumer, Supplier)?

Beginner
A functional interface has exactly one abstract method, making it eligible for lambda expression or method reference assignment -- Function<T,R> transforms an input of type T into an output of type R (apply()); Predicate<T> tests a condition, returning boolean (test()); Consumer<T> accepts an input and performs a side effect, returning nothing (accept()); Supplier<T> takes no input and produces a value (get()) -- these four generic interfaces cover the vast majority of common functional programming needs without requiring a custom interface for each specific case.
Function<String, Integer> length = String::length;
Predicate<String> isEmpty = String::isEmpty;
Consumer<String> printer = System.out::println;
Supplier<String> greeting = () -> "Hello";

System.out.println(length.apply("hello"));  // 5
System.out.println(isEmpty.test(""));         // true
Real-world example A validation pipeline uses Predicate<Order> to check business rules, Function<Order, Invoice> to transform an order into an invoice, and Consumer<Invoice> to send the resulting invoice via email, composing a complete order-processing flow entirely out of these standard functional interfaces without defining any custom ones.

Common follow-ups: What's the difference between Function and UnaryOperator, given both seem to take and return one value?;Why do these interfaces exist as generic interfaces rather than the JDK using raw Object types?

Streams & Lambdas;Generics

What are the four kinds of method references in Java (static, instance-bound, instance-unbound, and constructor), and how does the compiler determine which form applies?

Intermediate
Static method reference (ClassName::staticMethod) refers to a static method; bound instance method reference (instance::method) refers to a specific, already-existing object's instance method; unbound instance method reference (ClassName::instanceMethod) refers to an instance method where the first functional interface parameter becomes the receiver object the method is called on; constructor reference (ClassName::new) refers to a constructor -- the compiler determines which form applies based on the functional interface's method signature being targeted, matching parameter types and count against the referenced method/constructor's own signature.
// Static
Function<String, Integer> parse = Integer::parseInt;

// Bound instance (specific object 'str')
String str = "hello";
Supplier<Integer> len = str::length;

// Unbound instance (first param becomes the receiver)
Function<String, Integer> len2 = String::length;  // equivalent to s -> s.length()

// Constructor
Supplier<ArrayList<String>> listMaker = ArrayList::new;
Real-world example A stream pipeline transforming a list of raw strings into parsed integers uses the static method reference Integer::parseInt, while a separate pipeline extracting each string's length uses the unbound instance reference String::length, both more concise and readable than the equivalent explicit lambda expressions.

Common follow-ups: How would you write the unbound instance method reference example as an equivalent explicit lambda?;What happens if a method reference is ambiguous between multiple overloaded methods with the same name?

Streams & Lambdas;OOP & Classes

How does a lambda expression capture variables from its enclosing scope, and why must captured local variables be effectively final?

Advanced
A lambda capturing a local variable from its enclosing method doesn't actually capture the variable itself, but rather captures a copy of its value at the time the lambda is created (for local variables; instance/static fields are captured by reference to the containing object, allowing genuine mutation) -- Java enforces that any captured local variable must be effectively final (never reassigned after initialization, even if not explicitly marked final) specifically because the lambda might execute later, on a different thread, after the enclosing method has already returned and its local stack frame no longer exists, so allowing the lambda to observe or modify a since-changed local variable would create ill-defined, thread-unsafe semantics.
public Supplier<Integer> makeCounter() {
    int count = 0;
    // count++; // if uncommented, would cause a COMPILE ERROR: count is no longer effectively final
    return () -> count;  // captures the VALUE of count (0) at lambda creation time
}

// Workaround for genuine mutable state: use an array or an AtomicInteger as a container
AtomicInteger counter = new AtomicInteger(0);
Runnable increment = counter::incrementAndGet;  // captures the reference, container itself is mutable
Real-world example A background task submitted with a lambda capturing a local loop variable in a for-loop is a classic pitfall (fixed in Java 8+ since each iteration effectively creates a fresh variable binding for enhanced for-loops, unlike some older languages), but attempting to have multiple lambdas share and mutate a captured counter directly requires wrapping it in a mutable container like AtomicInteger, since the raw local variable itself cannot be reassigned once captured.

Common follow-ups: Why does capturing an instance field (via 'this') not have the same effectively-final restriction that local variables do?;What's the specific mechanism (variable capture into a synthetic final copy) the compiler uses under the hood to implement this?

Concurrency & Threads;Java Fundamentals: Syntax Data Types & Operators

How do the default methods andThen() and compose() on Function work, and what's the difference between them for chaining transformations?

Intermediate
andThen(after) creates a composed function that first applies the original function, then applies the 'after' function to that result (f.andThen(g) means g(f(x))); compose(before) instead creates a composed function that first applies the 'before' function, then applies the original (f.compose(g) means f(g(x))) -- the key distinction is the ORDER of application: andThen reads left-to-right matching the chain's declaration order, while compose reads right-to-left, and choosing the wrong one for your intended order produces a function that runs its steps in the opposite sequence from what you intended.
Function<Integer, Integer> addOne = x -> x + 1;
Function<Integer, Integer> multiplyByTwo = x -> x * 2;

Function<Integer, Integer> addThenMultiply = addOne.andThen(multiplyByTwo);
System.out.println(addThenMultiply.apply(3));  // (3+1)*2 = 8

Function<Integer, Integer> multiplyThenAdd = addOne.compose(multiplyByTwo);
System.out.println(multiplyThenAdd.apply(3));  // (3*2)+1 = 7 -- compose applies multiplyByTwo FIRST
Real-world example A data-transformation pipeline needing to first sanitize input then apply a business calculation chains sanitize.andThen(calculate) to express this order explicitly and readably, choosing andThen specifically because its left-to-right reading matches the intended sanitize-then-calculate sequence.

Common follow-ups: How would you chain more than two functions together using andThen() repeatedly?;Does Predicate have similar default methods (and(), or(), negate()) for combining boolean conditions?

Streams & Lambdas;Generics

How would you implement a custom functional interface with generic type parameters, and what considerations apply when designing one instead of reusing a built-in java.util.function interface?

Advanced
A custom functional interface is warranted specifically when a built-in interface's method name and semantic meaning wouldn't clearly communicate the interface's actual purpose in your domain (a domain-specific name like OrderValidator improves readability over a generic Predicate<Order>), or when you need a functional method with more than two parameters or a checked exception in its signature (which none of the standard java.util.function interfaces support directly) -- when designing one, marking it with @FunctionalInterface, keeping exactly one abstract method, and following the standard naming convention of an -er or descriptive verb-based name helps it integrate naturally alongside the JDK's own functional interfaces.
@FunctionalInterface
public interface TriFunction<A, B, C, R> {
    R apply(A a, B b, C c);  // built-in Function/BiFunction only support 1 or 2 parameters
}

TriFunction<Integer, Integer, Integer, Integer> sum3 = (a, b, c) -> a + b + c;
System.out.println(sum3.apply(1, 2, 3));  // 6
Real-world example A pricing calculation needing three inputs (base price, discount rate, tax rate) defines a custom TriFunction interface since none of the standard java.util.function interfaces support a three-argument function signature, filling a genuine gap in the built-in library's coverage.

Common follow-ups: Why does the standard library stop at BiFunction and not provide TriFunction/QuadFunction built in?;How would you design a functional interface whose method needs to declare a checked exception, given standard functional interfaces don't support this?

Generics;Interfaces & Abstract Classes

What is the difference between UnaryOperator<T> and Function<T,T>, and why does UnaryOperator exist as a separate, more specific interface?

Intermediate
UnaryOperator<T> extends Function<T,T> (a function whose input and output are the same type), existing as a more specific, semantically clearer name specifically for the common case where a transformation preserves its input's type (like a String-to-String transformation, or doubling a number) -- functionally it's interchangeable with Function<T,T> (inheriting the same apply() method plus andThen/compose), but using UnaryOperator communicates the same-type constraint more clearly to readers and provides a convenient static identity() factory method that Function alone doesn't offer with quite the same clarity.
UnaryOperator<String> toUpper = String::toUpperCase;
UnaryOperator<Integer> square = x -> x * x;

// Equivalent to Function<String, String> and Function<Integer, Integer>, but more semantically clear
UnaryOperator<String> identity = UnaryOperator.identity();  // returns its input unchanged
System.out.println(identity.apply("test"));  // "test"
Real-world example A List.replaceAll() call (which specifically requires a UnaryOperator<T> parameter, not just any Function<T,T>) is used to transform every element of a list in place, the API deliberately choosing the more semantically specific UnaryOperator type to communicate that the transformation must preserve the list's element type.

Common follow-ups: What's the equivalent 'BinaryOperator' interface, and what use case does it serve for combining two same-typed values?;Why does List.replaceAll() specifically require UnaryOperator rather than accepting a general Function?

Streams & Lambdas;Generics

How would you implement memoization (caching function results) as a higher-order function wrapping an existing Function, and what thread-safety considerations apply for a memoized function used concurrently?

Advanced
A memoizing wrapper takes an existing Function<T,R> and returns a new Function<T,R> that first checks an internal cache (typically a Map<T,R>) for a previously-computed result before falling back to invoking the original function and storing the result for future calls with the same input -- for thread-safe memoization under concurrent access, ConcurrentHashMap's computeIfAbsent() provides atomic check-then-compute semantics (avoiding a race where two threads redundantly compute the same expensive value simultaneously, and importantly avoiding the more subtle bug where the computation function itself recursively calls back into the same map during computeIfAbsent, which can cause a ConcurrentModificationException-like deadlock in certain JDK versions).
public static <T, R> Function<T, R> memoize(Function<T, R> function) {
    Map<T, R> cache = new ConcurrentHashMap<>();
    return input -> cache.computeIfAbsent(input, function);
}

Function<Integer, Long> slowFactorial = n -> { /* expensive computation */ return computeFactorial(n); };
Function<Integer, Long> fastFactorial = memoize(slowFactorial);
fastFactorial.apply(20);  // computed and cached
fastFactorial.apply(20);  // returned instantly from cache on second call
Real-world example An expensive recursive Fibonacci calculation function is wrapped with a generic memoize() utility, dramatically speeding up repeated calls with the same input across a request-handling application, using ConcurrentHashMap internally to remain safe under concurrent access from multiple request-handling threads.

Common follow-ups: What's the memory growth risk of an unbounded memoization cache, and how would you add eviction (like an LRU policy)?;Why can computeIfAbsent() with a recursive function cause issues in some JDK versions, and how would you avoid that pitfall?

Caching;Concurrency & Threads

How do the primitive-specialized functional interfaces (IntFunction, ToIntFunction, IntPredicate, IntUnaryOperator, etc.) avoid the performance cost of autoboxing, and when should you prefer them over the generic equivalents?

Intermediate
The generic Function<Integer, Integer> and similar boxed-type functional interfaces incur autoboxing/unboxing overhead on every invocation (wrapping a primitive int into an Integer object and back), which can be measurably significant in a performance-sensitive hot path processing large volumes of primitive data -- the primitive-specialized interfaces (IntFunction<R> takes a primitive int without boxing, ToIntFunction<T> returns a primitive int without boxing, IntUnaryOperator both takes and returns primitive int, etc.) avoid this boxing overhead entirely, making them the recommended choice specifically for performance-sensitive code operating on primitive numeric streams (like IntStream), while the generic versions remain perfectly fine for typical, non-performance-critical code.
// Generic version: boxing overhead on every call
Function<Integer, Integer> square1 = x -> x * x;

// Primitive-specialized: no boxing overhead
IntUnaryOperator square2 = x -> x * x;

IntStream.rangeClosed(1, 1_000_000)
    .map(square2)  // IntStream.map() specifically expects IntUnaryOperator, avoiding boxing entirely
    .sum();
Real-world example A numerical computation processing a stream of a million primitive integers uses IntStream combined with IntUnaryOperator throughout, avoiding the autoboxing overhead that using the generic Function<Integer,Integer> equivalent across a million elements would have measurably added to the computation's total runtime.

Common follow-ups: What specific primitive-specialized interfaces exist for long and double in addition to int?;How much actual performance difference does boxing avoidance make in practice for a typical workload size?

Streams & Lambdas;Diagnostics & Performance

How would you use BiFunction combined with Stream's reduce() to implement a custom aggregation, and how does this relate to the more general concept of a fold operation in functional programming?

Advanced
Stream.reduce() combines stream elements into a single result using a BinaryOperator<T> (a specialized BiFunction<T,T,T>) that takes an accumulated result so far and the next element, returning a new accumulated result -- this is a direct implementation of the classic functional programming 'fold' concept (also called reduce in many other languages), where reduce(identity, accumulator) processes elements left-to-right, starting from the identity value and repeatedly applying the accumulator function, letting you express custom aggregations (sum, concatenation, finding a maximum by custom criteria) declaratively rather than with an explicit mutable-accumulator loop.
List<String> words = List.of("Hello", "World", "Java");

String concatenated = words.stream()
    .reduce("", (acc, word) -> acc.isEmpty() ? word : acc + " " + word);
System.out.println(concatenated);  // "Hello World Java"

// Equivalent explicit imperative loop, which reduce() replaces declaratively:
// String result = "";
// for (String word : words) { result = result.isEmpty() ? word : result + " " + word; }
Real-world example A financial report aggregating a stream of transaction amounts into a running total with custom rounding logic applied at each step uses Stream.reduce() with a custom BinaryOperator<BigDecimal>, expressing the entire aggregation declaratively rather than as an explicit mutable-accumulator for-loop.

Common follow-ups: What's the difference between the two-argument and three-argument overloads of Stream.reduce()?;Why does reduce() require the combiner function to be associative for correct behavior with parallel streams?

Streams & Lambdas;Collections Framework

How does the Predicate interface's default methods and(), or(), and negate() enable composing complex boolean conditions from simpler ones?

Intermediate
and(other) creates a composed predicate that's true only if both the original and the other predicate are true (with short-circuit evaluation, matching && semantics); or(other) is true if either is true (short-circuiting like ||); negate() inverts the predicate's result -- these default methods let you build complex, readable filtering conditions by composing small, well-named predicates together rather than writing one large, harder-to-read boolean expression inline.
Predicate<String> isLongEnough = s -> s.length() >= 8;
Predicate<String> hasDigit = s -> s.chars().anyMatch(Character::isDigit);
Predicate<String> hasUpperCase = s -> s.chars().anyMatch(Character::isUpperCase);

Predicate<String> isStrongPassword = isLongEnough.and(hasDigit).and(hasUpperCase);
System.out.println(isStrongPassword.test("Password1"));  // true
Real-world example A password validation system composes several small, individually-named predicates (isLongEnough, hasDigit, hasUpperCase, hasSpecialChar) into one final isStrongPassword predicate using and(), making the overall validation logic self-documenting and each individual rule independently testable, rather than one large inline boolean expression.

Common follow-ups: How does short-circuit evaluation in and()/or() affect performance when the predicates involve expensive checks?;How would you build a list of predicates and combine them all with and() dynamically at runtime?

Streams & Lambdas;Testing Strategy

Showing 1–10 of 15