Five interfaces called “Checker”
Riya's canteen app now has PriceChecker, StockChecker, CouponChecker, TimeChecker and ItemChecker: five interfaces, each with one method that takes something and returns true or false.
Sir, every lambda needs an interface, so I keep making new ones. Is that how it's done?
All five have the same shape: something in, true or false out. Java already has that interface: Predicate<T>. And it can combine rules with and and or, which yours can't.
Are there ready-made ones for other shapes?
A whole package. Learn four shapes and you'll recognise the rest.
What is a functional interface?
A functional interface is an interface that has exactly one abstract method (a SAM, “single abstract method”), apart from public methods of Object. It may also have any number of default and static methods. Instances of functional interfaces can be created with lambda expressions, method references or constructor references. The optional annotation @FunctionalInterface makes the compiler verify the rule. The package java.util.function provides general-purpose functional interfaces such as Predicate<T>, Function<T,R>, Consumer<T> and Supplier<T>, plus two-argument and primitive specialisations.
A functional interface is a socket with one pin. Any lambda with the right shape plugs in.
Words you'll meet in this lesson
andThen.Class::method: a lambda that just calls one method.int into Integer; primitive interfaces avoid it.The four core shapes
| Interface | Method | In → out | Typical use |
|---|---|---|---|
Predicate<T> | test | T → boolean | filters, validation rules |
Function<T,R> | apply | T → R | conversion, calculation |
Consumer<T> | accept | T → nothing | print, save, send |
Supplier<T> | get | nothing → T | create, generate, lazy values |
Exam tip: “A functional interface has exactly one abstract method; it may have default and static methods. Examples: Runnable, Comparator, Callable, and Predicate/Function/Consumer/Supplier from java.util.function.” Write the method name of each one; examiners love that.
The java.util.function family
Predicate<T> boolean test(T t) filter, validate n -> n > 0
Function<T,R> R apply(T t) transform, convert s -> s.length()
Consumer<T> void accept(T t) print, save, send s -> System.out.println(s)
Supplier<T> T get() create, generate () -> new ArrayList<>()
BiFunction<T,U,R> R apply(T t, U u) combine two values (q, p) -> q * p
UnaryOperator<T> T apply(T t) same type in and out s -> s.trim()
BinaryOperator<T> T apply(T a, T b) reduce two to one (a, b) -> a + b
IntPredicate, IntFunction, ToIntFunction ... primitive versions (no boxing)Predicate
Combine with and, or, negate. Predicate.not(...) for method references.
Function
Chain with andThen and compose; Function.identity() returns its input.
Consumer
Chain with andThen to run several actions on the same value.
Supplier
Produces values on demand: new objects, ids, default values computed only when needed.
getFour kinds of method references
import java.util.*;
import java.util.function.*;
public class Main {
static boolean isValidPin(String pin) {
return pin.matches("\\d{6}");
}
public static void main(String[] args) {
Predicate<String> staticRef = Main::isValidPin; // 1. static method
String shop = "MyShop";
Supplier<Integer> boundRef = shop::length; // 2. method of a particular object
Function<String, String> unboundRef = String::toUpperCase; // 3. method of the argument
Supplier<List<String>> ctorRef = ArrayList::new; // 4. constructor
System.out.println(staticRef.test("411001") + " " + boundRef.get() + " "
+ unboundRef.apply("pune") + " " + ctorRef.get());
}
}Old interfaces count too. Runnable, Callable, Comparator and ActionListener were functional interfaces long before Java 8. That's why new Thread(() -> …) and list.sort((a, b) -> …) work.
How the compiler matches a lambda to an interface
Find the target type
The variable's type isPredicate<Integer> bigOrder = amt -> amt > 1000;Predicate<Integer>.Find its single abstract method
boolean test(Integer t). Default methods likeandare ignored.Check the lambda's shape
One parameter (soamtis an Integer), and the body must produce a boolean:amt > 1000does. Mismatches are compile errors.Create the object
At run time an object implementing Predicate is created; itstestruns the lambda body.Use the extras
Default methods build new Predicate objects that call the originals. Nothing is tested until you callPredicate<Integer> freeDelivery = bigOrder.or(amt -> amt == 0);test.
The four core interfaces: both views
Left: each interface as a machine with an input and an output. Right: what a functional interface looks like inside, and the objects your lambdas become.
- A Predicate takes a value and answers true or false: “is this order above ₹1,000?”. Its method is
test. - A Function takes a value and returns another, possibly of a different type: “add 18% GST”. Its method is
apply. - A Consumer takes a value and does something with it, returning nothing: “send this SMS”. Its method is
accept. - A Supplier takes nothing and produces a value whenever asked: “give me a new order id”. Its method is
get. - They come with default methods for combining.
andThenruns one function and feeds its result into the next. - You rarely need your own interface: ask what goes in and what comes out, then pick the matching built-in one.
- This is (simplified) how
java.util.function.Functionis declared. - It has exactly one abstract method,
apply. That's what makes it a functional interface, and what a lambda implements. - Default methods (with a body) and static methods don't count. They give extra abilities like chaining.
@FunctionalInterfaceasks the compiler to check the rule. - Assigning a lambda creates an object (#1) whose
applyruns the lambda body. - A method reference such as
String::lengthis a shorthand lambda: it also becomes an object implementing the interface. - Calling
andThenreturns a new Function object (#3) that remembers both functions and applies them one after the other.
Functional interfaces in real systems
Libraries and frameworks accept functional interfaces wherever you plug in your own logic.
Watch it with an example: a checkout pipeline built from small functions
A shop's checkout applies a coupon, then GST, then rounds the total. Each step is a Function; andThen joins them into one pipeline.
- The cart total is ₹849.
- The coupon Function subtracts ₹100: ₹749.
- The GST Function multiplies by 1.18: ₹883.82.
- Rounding gives ₹884. If GST were applied before the coupon, the customer would pay a different amount, so the order of andThen matters.
coupon.andThen(gst).andThen(round)is itself a Function. The shop can swap or add steps (a festival discount, delivery fee) without touching the others. (Illustrative rules.)
1. Validation and rules engines
Eligibility, fraud and coupon rules are written as Predicates and combined, so a new rule is one new line.
2. Data transformation
Mapping database rows to objects and objects to JSON uses Functions (often method references like Customer::fromRow).
3. Logging and lazy values
Loggers accept a Supplier<String> so an expensive message is only built if that log level is enabled.
4. Event listeners
“On payment success”, “on file uploaded” and “on message received” hooks are Consumers registered by different modules.
Simplified: real rules engines also log which rule decided, so decisions can be explained to customers and auditors.
All four interfaces on today's orders
import java.util.*;
import java.util.function.*;
public class Main {
public static void main(String[] args) {
List<Integer> orders = List.of(450, 1200, 80, 2600, 999);
Predicate<Integer> bigOrder = amt -> amt >= 1000;
Function<Integer, String> label = amt -> "Rs " + amt + (bigOrder.test(amt) ? " (free delivery)" : "");
Consumer<String> print = System.out::println;
Supplier<String> header = () -> "Today's orders:";
print.accept(header.get());
for (int amt : orders) {
print.accept(label.apply(amt));
}
long big = orders.stream().filter(bigOrder).count();
print.accept(big + " orders get free delivery");
}
}Predicate<Integer> bigOrderUsed twice: inside the label, and as a filter for the stream.Function<Integer, String> labelTurns an amount into display text: different input and output types.Consumer<String> print = System.out::printlnA bound method reference to theprintlnofSystem.out.Supplier<String> headerProduces the heading only whenget()is called.
test, apply, get and accept
Recorded from the real JVM. Four lambda objects appear on the heap; each call to test, apply, get or accept runs its lambda body as a new frame.
Worked examples: real problems, solved and traced in memory
Three problems solved by plugging lambdas into the standard interfaces: Predicate rules combined with and/or, a Function pipeline, and method references for everyday conversions.
Loan eligibility with combined Predicates
Problem statement
A bank pre-approves a loan if the applicant earns at least ₹30,000 a month and has a credit score of 700+, or is an existing customer with a score of 750+. Express each rule as a Predicate and combine them.
Asha (45000, 720, new), Ravi (25000, 780, existing), Imran (28000, 690, new)
Asha: pre-approved Ravi: pre-approved Imran: not eligible
Approach
- A small
Applicantclass with income, score and existing flag. - One Predicate per rule; combine with
and()andor(). - Test each applicant.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Price pipeline with Function.andThen
Problem statement
A marketplace computes a seller's payout: sale price minus 12% commission, minus ₹20 shipping fee, then rounded down to whole rupees. Build it as a chain of Functions and apply it to three sales.
sales 999, 1500, 250
sale Rs 999 -> payout Rs 859 sale Rs 1500 -> payout Rs 1300 sale Rs 250 -> payout Rs 200
Approach
- Three small Functions: commission, shipping, rounding.
- Join them with
andTheninto onepayoutFunction. - Apply it to each sale.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Method references for everyday conversions
Problem statement
A CRM import has messy customer names. Trim them, convert to upper case, skip blanks, and create a Customer for each, using method references instead of lambdas.
" asha ", "", " ravi", "meena "
[ASHA] [RAVI] [MEENA]
Approach
String::trimandString::toUpperCaseas Functions.String::isBlankas a Predicate (negated).Customer::newas a Function that calls the constructor.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Check yourself
Quick revision
Lesson 14.2 in one screen
- Functional interface = exactly one abstract method; default/static methods allowed.
@FunctionalInterfacemakes the compiler check it.- Predicate
test(T→boolean), Functionapply(T→R), Consumeraccept(T→void), Supplierget(()→T). - Bi- versions take two inputs; UnaryOperator/BinaryOperator keep one type; Int-/Long-/Double- versions avoid boxing.
- Combine:
and/or/negate,andThen/compose,Consumer.andThen. - Method references:
Class::staticMethod,obj::method,Class::instanceMethod,Class::new.
Interview and viva questions
What is a functional interface? Give examples.
An interface with exactly one abstract method, which lambdas and method references can implement. Examples: Runnable (run), Callable (call), Comparator (compare), Predicate (test), Function (apply), Consumer (accept), Supplier (get).
Comparator has equals() too. Why is it still functional?
Abstract methods that match public methods of Object (like equals) don't count, because every implementation already inherits them from Object. Only compare counts.
Difference between Function and UnaryOperator?
UnaryOperator<T> extends Function<T,T>: same input and output type. It's just a more specific, more readable name for that case, e.g. String::trim.
What are the types of method references?
Static method (Math::max), instance method of a particular object (System.out::println), instance method of an arbitrary object of a type (String::length, where the first argument becomes the receiver), and constructor (ArrayList::new).
Why use Supplier for default values?
Laziness: a Supplier only computes its value when get() is called. Methods like Optional.orElseGet(supplier) and logging APIs use this to avoid expensive work that might not be needed.
Practice problems
Pick the right built-in interface, write it as a lambda or method reference, and combine with default methods. Outputs are fixed. The last three are business tasks.
Predicate: is it even?
Write a Predicate<Integer> that checks for even numbers and test 4 and 7.
4, 7
true false
Show solution and explanation
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
Predicate<Integer> even = n -> n % 2 == 0;
System.out.println(even.test(4) + " " + even.test(7));
}
}How it works: Predicate's single method is boolean test(T t).
Function: name length
Use Function<String,Integer> with a method reference to get the length of "Nagpur".
Nagpur
6
Show solution and explanation
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Function<String, Integer> len = String::length;
System.out.println(len.apply("Nagpur"));
}
}How it works: String::length is the same as s -> s.length(): the input becomes the object the method is called on.
Supplier and Consumer
A Supplier gives today's greeting; a Consumer prints it with a prefix.
(none)
>> Good morning
Show solution and explanation
import java.util.function.*;
public class Main {
public static void main(String[] args) {
Supplier<String> greeting = () -> "Good morning";
Consumer<String> show = s -> System.out.println(">> " + s);
show.accept(greeting.get());
}
}How it works: Supplier: nothing in, value out (get). Consumer: value in, nothing out (accept).
andThen vs compose
With gst = p -> p * 118 / 100 and off = p -> p - 50, print gst.andThen(off) and gst.compose(off) for 1000.
1000
gst then off: 1130 off then gst: 1121
Show solution and explanation
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Function<Integer, Integer> gst = p -> p * 118 / 100;
Function<Integer, Integer> off = p -> p - 50;
System.out.println("gst then off: " + gst.andThen(off).apply(1000));
System.out.println("off then gst: " + gst.compose(off).apply(1000));
}
}How it works: f.andThen(g) = g(f(x)); f.compose(g) = f(g(x)). Discount before tax gives a smaller bill.
BiFunction and BinaryOperator
Use a BiFunction<Integer,Integer,Integer> for the bill of qty × price, and a BinaryOperator<Integer> (Integer::max) to find the larger of two bills.
3 × 120, 2 × 199
360 vs 398 -> larger 398
Show solution and explanation
import java.util.function.*;
public class Main {
public static void main(String[] args) {
BiFunction<Integer, Integer, Integer> bill = (qty, price) -> qty * price;
BinaryOperator<Integer> larger = Integer::max;
int a = bill.apply(3, 120), b = bill.apply(2, 199);
System.out.println(a + " vs " + b + " -> larger " + larger.apply(a, b));
}
}How it works: Bi- versions take two arguments. BinaryOperator<T> is a BiFunction whose inputs and output are all the same type.
Your own @FunctionalInterface
Declare @FunctionalInterface interface Converter { double convert(double inr); } and use it for USD and EUR rates.
₹1000; rates 0.012, 0.011
USD 12.00, EUR 11.00
Show solution and explanation
@FunctionalInterface
interface Converter {
double convert(double inr);
}
public class Main {
public static void main(String[] args) {
Converter toUsd = inr -> inr * 0.012;
Converter toEur = inr -> inr * 0.011;
System.out.printf("USD %.2f, EUR %.2f%n", toUsd.convert(1000), toEur.convert(1000));
}
}How it works: a custom interface gives a domain name to the behaviour. The annotation makes the compiler reject a second abstract method. (Illustrative rates.)
Coupon validity rules
A coupon applies if the cart is ₹499+ and the user is new, or if it's a weekend. Combine Predicates on a Cart and test three carts.
3 carts
Rs 650: coupon OK Rs 300: no coupon Rs 120: coupon OK
Show solution and explanation
import java.util.function.Predicate;
class Cart {
int total;
boolean newUser, weekend;
Cart(int t, boolean n, boolean w) { total = t; newUser = n; weekend = w; }
}
public class Main {
public static void main(String[] args) {
Predicate<Cart> bigCart = c -> c.total >= 499;
Predicate<Cart> newUser = c -> c.newUser;
Predicate<Cart> weekend = c -> c.weekend;
Predicate<Cart> applies = bigCart.and(newUser).or(weekend);
Cart[] carts = {new Cart(650, true, false), new Cart(300, true, false), new Cart(120, false, true)};
for (Cart c : carts) System.out.println("Rs " + c.total + ": " + (applies.test(c) ? "coupon OK" : "no coupon"));
}
}How it works: business rules as small named Predicates are easy to read, test and change one at a time.
Order id generator
Write a Supplier<String> that returns ids ORD-1001, ORD-1002… each time it's called. Generate three.
(none)
ORD-1001 ORD-1002 ORD-1003
Show solution and explanation
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
public class Main {
public static void main(String[] args) {
AtomicInteger counter = new AtomicInteger(1000);
Supplier<String> nextId = () -> "ORD-" + counter.incrementAndGet();
for (int i = 0; i < 3; i++) System.out.println(nextId.get());
}
}How it works: the lambda can't change a captured int, but it can call methods on a captured object. AtomicInteger also makes the generator safe if many threads call it.
Notification fan-out with Consumer.andThen
When a payment succeeds, send an SMS, then an email, then write an audit log, using one combined Consumer.
payment PAY-77, Rs 640
SMS sent for PAY-77 Rs 640 Email sent for PAY-77 Rs 640 Audit: PAY-77 Rs 640 logged
Show solution and explanation
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
Consumer<String> sms = p -> System.out.println("SMS sent for " + p);
Consumer<String> email = p -> System.out.println("Email sent for " + p);
Consumer<String> audit = p -> System.out.println("Audit: " + p + " logged");
Consumer<String> onSuccess = sms.andThen(email).andThen(audit);
onSuccess.accept("PAY-77 Rs 640");
}
}How it works: Consumer.andThen calls each consumer in order with the same input. Adding a WhatsApp message later is one more andThen.
Real-world scenario questions (asked in company interviews)
These check whether you can use the standard interfaces to keep code small and flexible.
A 200-line if-else for discounts
Discount logic is one giant method with nested ifs for festival offers, first orders, bank cards and city promos. Marketing adds a new rule every week. How would you restructure it with functional interfaces?
The interviewer is checking: rules as data.
Show a strong answer
- Model each rule as a pair: a
Predicate<Order>(does it apply?) and aFunction<Order, Discount>(how much?). - Keep rules in a list (or load them from config); evaluate them in order or pick the best.
- Each rule becomes independently testable, and adding one doesn't touch the others.
Avoid: inventing a new interface for every rule type when Predicate and Function fit.
A hot loop creating millions of Integers
A pricing engine applies Function<Integer,Integer> to 50 million prices a minute. Profiling shows lots of time in garbage collection of Integer objects. What change would you make?
The interviewer is checking: primitive functional interfaces.
Show a strong answer
- Each apply boxes the int argument and result into Integer objects.
- Use
IntUnaryOperator(orIntFunction,ToIntFunction,IntStream) to stay with primitives. - Measure again; primitive streams and arrays usually remove most of the GC pressure.
Avoid: tuning the GC first; remove the allocation.
Debug logs slowing down production
log.debug("cart: " + cart.toJson()) runs on every request. Debug logging is off in production, yet the server spends a lot of time in toJson(). Why, and how do you fix it?
The interviewer is checking: lazy evaluation with Supplier.
Show a strong answer
- The argument string is built before
debugis even called, whether or not it's logged. - Pass a
Supplier<String>:log.debug(() -> "cart: " + cart.toJson()). The logger callsget()only if debug is enabled.
Avoid: deleting the logs; you'll want them when debugging.
Should we create our own interface?
A teammate wants to add interface TaxCalculator { BigDecimal calculate(Invoice i); } although Function<Invoice, BigDecimal> has the same shape. Which would you choose?
The interviewer is checking: trade-offs, not dogma.
Show a strong answer
- For internal, short-lived plumbing,
Functionis enough and composes withandThen. - For a core domain concept used across the codebase, a named interface documents intent, can carry Javadoc, checked exceptions or extra default methods, and avoids mixing up two Functions with the same types.
- Mark it
@FunctionalInterfaceso lambdas still work.
Avoid: “always” or “never” answers.