Functional interfaces

A lambda needs a target: an interface with exactly one abstract method. Java ships dozens of these in java.util.function, so you almost never write your own. You'll learn the four you'll use daily (Predicate, Function, Consumer, Supplier), how to combine them into rules and pipelines, and method references, the shortest way to write a lambda.

Module 14 · Modern Java⏱ 22 minIntermediate9 practice problems
1 The story

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.

PriceChecker StockChecker CouponChecker TimeChecker ItemChecker all: T → boolean Predicate<T> built in, with and / or / negate myphatshala.in
Riya

Sir, every lambda needs an interface, so I keep making new ones. Is that how it's done?

Arjun Sir

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.

Riya

Are there ready-made ones for other shapes?

Arjun Sir

A whole package. Learn four shapes and you'll recognise the rest.

2 Definition

What is a functional interface?

Formal definition (write this in exams)

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.

In simple words

A functional interface is a socket with one pin. Any lambda with the right shape plugs in.

Words you'll meet in this lesson

SAMSingle abstract method: the one method a lambda implements.
@FunctionalInterfaceOptional check that the interface has exactly one abstract method.
Default methodAn interface method with a body, e.g. andThen.
Method referenceClass::method: a lambda that just calls one method.
CompositionJoining small functions into a bigger one.
BoxingWrapping int into Integer; primitive interfaces avoid it.

The four core shapes

InterfaceMethodIn → outTypical use
Predicate<T>testT → booleanfilters, validation rules
Function<T,R>applyT → Rconversion, calculation
Consumer<T>acceptT → nothingprint, save, send
Supplier<T>getnothing → Tcreate, 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.

3 Types and ways to use it

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.

test

Function

Chain with andThen and compose; Function.identity() returns its input.

apply

Consumer

Chain with andThen to run several actions on the same value.

accept

Supplier

Produces values on demand: new objects, ids, default values computed only when needed.

get

Four 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());
    }
}
true 6 PUNE []

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.

4 Step by step

How the compiler matches a lambda to an interface

  1. Find the target type

    Predicate<Integer> bigOrder = amt -> amt > 1000;
    The variable's type is Predicate<Integer>.
  2. Find its single abstract method

    boolean test(Integer t). Default methods like and are ignored.
  3. Check the lambda's shape

    One parameter (so amt is an Integer), and the body must produce a boolean: amt > 1000 does. Mismatches are compile errors.
  4. Create the object

    At run time an object implementing Predicate is created; its test runs the lambda body.
  5. Use the extras

    Predicate<Integer> freeDelivery = bigOrder.or(amt -> amt == 0);
    Default methods build new Predicate objects that call the originals. Nothing is tested until you call test.
5 See it happen

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.

The four core functional interfaces, both views 6 steps
What you see Predicate<Integer> big = amt -> amt > 1000; Function<Integer,Integer> gst = amt -> amt * 118 / 100; Consumer<String> sms = msg -> send(msg); Supplier<String> orderId = () -> "ORD-" + next(); gst.andThen(a -> a - 50).apply(1000) // pick the interface by its shape Predicate T → boolean 1500 → true Function T → R 1000 → 1180 Consumer T → (nothing) "Paid" → SMS sent Supplier () → T → "ORD-102" gst a -> a - 50 1000 → 1180 → 1130 test a condition? Predicate turn A into B? Function use it, return nothing? Consumer make one from nothing? Supplier Predicate: yes or no. Function: transform. Consumer: use, return nothing. Supplier: produce on demand. Functions can be chained. Choose by input and output. myphatshala.in
Behind the screen @FunctionalInterface interface Function<T, R> { R apply(T t); // the SAM default <V> Function<T,V> andThen(Function<R,V> after) static <T> Function<T,T> identity() } HEAP #1 lambda (Function) #2 method ref String::length #3 andThen holds #1 and the next fn counts as functional: 1 abstract An interface from java.util.function. Exactly one abstract method. Default and static methods are extra. A lambda supplies apply(). So can a method reference. andThen builds a new Function. myphatshala.in
6 Real-world use

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.

Checkout as a chain of Functions 5 steps
Animated example coupon a -> a - 100 gst a -> a * 1.18 round Math::round ₹849 ₹749 ₹883.82 ₹884 checkout = coupon .andThen( gst) .andThen( round) the order of steps changes the answer Cart total ₹849. Coupon: ₹100 off. GST 18%. Rounded: ₹884. One Function built from three. myphatshala.in

1. Validation and rules engines

Eligibility, fraud and coupon rules are written as Predicates and combined, so a new rule is one new line.

loan application→rules.and(...).or(...)→approve / reject

2. Data transformation

Mapping database rows to objects and objects to JSON uses Functions (often method references like Customer::fromRow).

row→Function<Row, Customer>→object

3. Logging and lazy values

Loggers accept a Supplier<String> so an expensive message is only built if that log level is enabled.

log.debug(() -> …)→Supplier.get() only if needed→no wasted work

4. Event listeners

“On payment success”, “on file uploaded” and “on message received” hooks are Consumers registered by different modules.

payment event→Consumer<Payment>s→SMS, email, audit

Simplified: real rules engines also log which rule decided, so decisions can be explained to customers and auditors.

7 Run the code

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");
    }
}
Today's orders: Rs 450 Rs 1200 (free delivery) Rs 80 Rs 2600 (free delivery) Rs 999 2 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 the println of System.out.
  • Supplier<String> headerProduces the heading only when get() is called.
8 Line by line

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.

Example 1Banking · loans

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.

Input
Asha (45000, 720, new), Ravi (25000, 780, existing), Imran (28000, 690, new)
Expected output
Asha: pre-approved
Ravi: pre-approved
Imran: not eligible

Approach

  1. A small Applicant class with income, score and existing flag.
  2. One Predicate per rule; combine with and() and or().
  3. 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.

Example 2E-commerce · pricing

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.

Input
sales 999, 1500, 250
Expected output
sale Rs 999 -> payout Rs 859
sale Rs 1500 -> payout Rs 1300
sale Rs 250 -> payout Rs 200

Approach

  1. Three small Functions: commission, shipping, rounding.
  2. Join them with andThen into one payout Function.
  3. 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.

Example 3Data · cleanup

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.

Input
"  asha ", "", " ravi", "meena  "
Expected output
[ASHA]
[RAVI]
[MEENA]

Approach

  1. String::trim and String::toUpperCase as Functions.
  2. String::isBlank as a Predicate (negated).
  3. Customer::new as 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.

9 Quiz and revision

Check yourself

Quick revision

Lesson 14.2 in one screen

  • Functional interface = exactly one abstract method; default/static methods allowed.
  • @FunctionalInterface makes the compiler check it.
  • Predicate test (T→boolean), Function apply (T→R), Consumer accept (T→void), Supplier get (()→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.

P1Easy

Predicate: is it even?

Write a Predicate<Integer> that checks for even numbers and test 4 and 7.

Input
4, 7
Expected output
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));
    }
}
true false

How it works: Predicate's single method is boolean test(T t).

P2Easy

Function: name length

Use Function<String,Integer> with a method reference to get the length of "Nagpur".

Input
Nagpur
Expected output
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"));
    }
}
6

How it works: String::length is the same as s -> s.length(): the input becomes the object the method is called on.

P3Easy

Supplier and Consumer

A Supplier gives today's greeting; a Consumer prints it with a prefix.

Input
(none)
Expected output
>> 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());
    }
}
>> Good morning

How it works: Supplier: nothing in, value out (get). Consumer: value in, nothing out (accept).

P4Medium

andThen vs compose

With gst = p -> p * 118 / 100 and off = p -> p - 50, print gst.andThen(off) and gst.compose(off) for 1000.

Input
1000
Expected output
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));
    }
}
gst then off: 1130 off then gst: 1121

How it works: f.andThen(g) = g(f(x)); f.compose(g) = f(g(x)). Discount before tax gives a smaller bill.

P5Medium

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.

Input
3 × 120, 2 × 199
Expected output
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));
    }
}
360 vs 398 -> larger 398

How it works: Bi- versions take two arguments. BinaryOperator<T> is a BiFunction whose inputs and output are all the same type.

P6Medium

Your own @FunctionalInterface

Declare @FunctionalInterface interface Converter { double convert(double inr); } and use it for USD and EUR rates.

Input
₹1000; rates 0.012, 0.011
Expected output
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));
    }
}
USD 12.00, EUR 11.00

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.)

P7Business problem

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.

Input
3 carts
Expected output
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"));
    }
}
Rs 650: coupon OK Rs 300: no coupon Rs 120: coupon OK

How it works: business rules as small named Predicates are easy to read, test and change one at a time.

P8Business problem

Order id generator

Write a Supplier<String> that returns ids ORD-1001, ORD-1002… each time it's called. Generate three.

Input
(none)
Expected output
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());
    }
}
ORD-1001 ORD-1002 ORD-1003

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.

P9Business problem

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.

Input
payment PAY-77, Rs 640
Expected output
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");
    }
}
SMS sent for PAY-77 Rs 640 Email sent for PAY-77 Rs 640 Audit: PAY-77 Rs 640 logged

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.

Design · rulesJunior

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 a Function<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.

Performance · boxingMid

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 (or IntFunction, 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.

Logging · performanceJunior

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 debug is even called, whether or not it's logged.
  • Pass a Supplier<String>: log.debug(() -> "cart: " + cart.toJson()). The logger calls get() only if debug is enabled.

Avoid: deleting the logs; you'll want them when debugging.

API designMid

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, Function is enough and composes with andThen.
  • 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 @FunctionalInterface so lambdas still work.

Avoid: “always” or “never” answers.