Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 9 – Lambdas, functional interfaces and Optional

Functional interfaces in java.util.function

Use Function, Predicate, Consumer, Supplier and their primitive forms, compose them with andThen and and, and declare your own functional interfaces.

  • Intermediate
  • 18 minutes
  • Examples run with Java 25.0.4 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 18 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Use Function, Predicate, Supplier, Consumer and their primitive variants
  • Compose functions with andThen, compose, and, or and negate
  • Declare a functional interface with @FunctionalInterface

Before you start

What you’ll learn · Before you start

What you will learn

  • Use Function, Predicate, Supplier, Consumer and their primitive variants
  • Compose functions with andThen, compose, and, or and negate
  • Declare a functional interface with @FunctionalInterface

Before you start

On this page

A functional interface is an interface with exactly one abstract method, and every lambda and method reference in Java is an object of one. You can write your own, but you rarely need to: the package java.util.function holds 43 ready-made ones, all built on four shapes. A Function<T, R> turns a T into an R. A Predicate<T> answers true or false about a T. A Consumer<T> does something with a T and returns nothing. A Supplier<T> produces a T from nothing. The rest of the package varies those shapes: two inputs (BiFunction), a result of the input’s type (UnaryOperator), and primitive versions such as IntPredicate that work on int values without boxing them into Integer objects. Most of the interfaces also carry default methods, such as andThen and and, that combine small functions into bigger ones. This lesson uses each shape, counts the whole family on JDK 25, composes rules, and writes a functional interface of its own where the standard ones fall short.

The four basic shapes

Shapes.java Java · Shapes.java
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

class Shapes {
    static void main() {
        Function<String, Integer> length = s -> s.length();             // T -> R
        Predicate<String> blank = s -> s.isBlank();                     // T -> boolean
        Consumer<String> shout = s -> IO.println(s.toUpperCase());      // T -> nothing
        Supplier<List<String>> newList = () -> new ArrayList<>();       // nothing -> T
        UnaryOperator<String> strip = s -> s.strip();                   // T -> T
        BinaryOperator<Integer> larger = (a, b) -> Math.max(a, b);      // (T, T) -> T
        BiFunction<String, Integer, String> dots = (s, n) -> s + ".".repeat(n);   // (T, U) -> R

        IO.println(length.apply("lambda"));
        IO.println(blank.test("   "));
        shout.accept("consumed");
        List<String> list = newList.get();
        list.add("first");
        IO.println(list);
        IO.println("[" + strip.apply("  padded  ") + "]");
        IO.println(larger.apply(82, 91));
        IO.println(dots.apply("Asha", 3));
    }
}

Each of the six interfaces in the program has one abstract method, and the method’s shape says where it fits:

  • Function<T, R>, method R apply(T t): a value in, a value out. map in a stream takes one, and so does any conversion from one type to another.
  • Predicate<T>, method boolean test(T t): a value in, true or false out. filter takes one, and validation rules are predicates.
  • Consumer<T>, method void accept(T t): a value in, nothing out. forEach takes one, as do logging and writing.
  • Supplier<T>, method T get(): nothing in, a value out. Defaults made only when needed and factories are suppliers.
  • UnaryOperator<T>, method T apply(T t): a Function<T, T>. List.replaceAll takes one, and so do text clean-up steps.
  • BinaryOperator<T>, method T apply(T a, T b): a BiFunction<T, T, T>. reduce takes one to merge two values.

UnaryOperator<T> and BinaryOperator<T> add no new method: they extend Function and BiFunction and only fix the result type to the input type, which documents intent and saves a type argument. Choosing the narrowest shape also documents intent. A method that takes a Predicate<Order> can only ask a yes-or-no question about an order; one that takes a Function<Order, Object> could mean anything.

The whole family, counted on JDK 25

The package is regular enough to list by program. This one reads the JDK’s own class files through the jrt: file system, checks that each type is an interface carrying @FunctionalInterface, and groups the names by the shape their name ends with:

Family.java Java · Family.java
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

class Family {
    static final List<String> SHAPES = List.of("Function", "Operator", "Predicate", "Consumer", "Supplier");

    static void main() throws Exception {
        // The JDK's own class files, read through the jrt: file system of the running Java.
        FileSystem jrt = FileSystems.getFileSystem(URI.create("jrt:/"));
        List<String> names;
        try (Stream<Path> files = Files.list(jrt.getPath("/modules/java.base/java/util/function"))) {
            names = files.map(p -> p.getFileName().toString())
                    .filter(n -> n.endsWith(".class") && !n.contains("-"))
                    .map(n -> n.substring(0, n.length() - ".class".length()))
                    .sorted()
                    .toList();
        }
        int annotated = 0;
        for (String n : names) {
            Class<?> c = Class.forName("java.util.function." + n);
            if (c.isInterface() && c.isAnnotationPresent(FunctionalInterface.class)) annotated++;
        }
        IO.println(names.size() + " types in java.util.function, " + annotated + " of them @FunctionalInterface interfaces");

        Map<String, List<String>> byShape = new LinkedHashMap<>();
        for (String shape : SHAPES) byShape.put(shape, new ArrayList<>());
        for (String n : names) {
            for (String shape : SHAPES) {
                if (n.endsWith(shape)) {
                    byShape.get(shape).add(n);
                    break;
                }
            }
        }
        byShape.forEach((shape, list) -> {
            IO.println();
            IO.println(shape + " shape, " + list.size() + " types:");
            print(list);
        });
    }

    /** Prints names separated by commas, in lines of at most 64 characters. */
    static void print(List<String> list) {
        StringBuilder line = new StringBuilder();
        for (String n : list) {
            if (!line.isEmpty() && line.length() + n.length() + 2 > 64) {
                IO.println(line + ",");
                line.setLength(0);
            }
            if (!line.isEmpty()) line.append(", ");
            line.append(n);
        }
        IO.println(line);
    }
}
The four basic shapes Function, Predicate, Consumer and Supplier, and the prefixes and suffixes that name the other 39 interfaces.Four basic shapesNames built on the shapesFunction<T, R> R apply(T t)one value in, one value outPredicate<T> boolean test(T t)one value in, true or false outConsumer<T> void accept(T t)one value in, nothing outSupplier<T> T get()nothing in, one value outBi…: two inputsBiFunction<T, U, R>, BiPredicate, BiConsumer…Operator: the result has the input's typeUnaryOperator<T>, BinaryOperator<T>Int…, Long…, Double…: primitive inputsIntPredicate, LongConsumer, DoubleFunction<R>To…: a primitive resultToIntFunction<T>, IntToLongFunctionObj…: an object, then a primitiveObjIntConsumer<T>prefixes and suffixesgive the other 39

The shapes of java.util.function and how the other names are built

Text description of the diagram

The diagram has two groups, one above the other, joined by an arrow labelled "prefixes and suffixes give the other 39".

The first group lists the four basic shapes: 1. Function<T, R>, with the method R apply(T t): one value in, one value out. 2. Predicate<T>, with the method boolean test(T t): one value in, true or false out. 3. Consumer<T>, with the method void accept(T t): one value in, nothing out. 4. Supplier<T>, with the method T get(): nothing in, one value out.

The second group lists the naming rules: 1. A Bi prefix means two inputs, as in BiFunction<T, U, R>, BiPredicate and BiConsumer. 2. An Operator suffix means the result has the same type as the inputs, as in UnaryOperator<T> and BinaryOperator<T>. 3. An Int, Long or Double prefix means primitive inputs, as in IntPredicate, LongConsumer and DoubleFunction<R>. 4. A To prefix means a primitive result, as in ToIntFunction<T> and IntToLongFunction. 5. An Obj prefix means an object followed by a primitive, as in ObjIntConsumer<T>.

The four shapes in use, then all 43 interfaces by shape Python · shapes_session.py
from session import run

run("""
java Shapes.java
java Family.java
""")

Output

# javac 25.0.4.1
$ java Shapes.java
6
true
CONSUMED
[first]
[padded]
91
Asha...
$ java Family.java
43 types in java.util.function, 43 of them @FunctionalInterface interfaces

Function shape, 17 types:
BiFunction, DoubleFunction, DoubleToIntFunction,
DoubleToLongFunction, Function, IntFunction, IntToDoubleFunction,
IntToLongFunction, LongFunction, LongToDoubleFunction,
LongToIntFunction, ToDoubleBiFunction, ToDoubleFunction,
ToIntBiFunction, ToIntFunction, ToLongBiFunction, ToLongFunction

Operator shape, 8 types:
BinaryOperator, DoubleBinaryOperator, DoubleUnaryOperator,
IntBinaryOperator, IntUnaryOperator, LongBinaryOperator,
LongUnaryOperator, UnaryOperator

Predicate shape, 5 types:
BiPredicate, DoublePredicate, IntPredicate, LongPredicate,
Predicate

Consumer shape, 8 types:
BiConsumer, Consumer, DoubleConsumer, IntConsumer, LongConsumer,
ObjDoubleConsumer, ObjIntConsumer, ObjLongConsumer

Supplier shape, 5 types:
BooleanSupplier, DoubleSupplier, IntSupplier, LongSupplier,
Supplier

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shapes_session.py

43 types, every one an interface annotated @FunctionalInterface: 17 shaped like Function, 8 operators, 5 predicates, 8 consumers and 5 suppliers. The names follow rules that the package documentation spells out, so you can read any of them without looking it up:

  • Bi means two inputs: BiFunction<T, U, R>, BiPredicate<T, U>, BiConsumer<T, U>.
  • Int, Long, Double at the start mean primitive inputs: IntPredicate tests an int.
  • To… means a primitive result: ToIntFunction<T> turns a T into an int, and IntToLongFunction an int into a long.
  • Obj keeps the first input an object: ObjIntConsumer<T> accepts a T and an int.

Only int, long and double get versions of their own, plus boolean as a result (BooleanSupplier). There is no CharPredicate or FloatFunction: a char or float widens to int or double and uses those.

Primitive versions avoid boxing

Function<Integer, Integer> cannot hold an int. Generics take only reference types, so every argument and every result is boxed into an Integer object and unboxed again. IntUnaryOperator does the same arithmetic on plain int values:

Boxing.java Java · Boxing.java
import java.util.function.Function;
import java.util.function.IntUnaryOperator;

class Boxing {
    static void main() {
        Function<Integer, Integer> boxed = x -> x * 2;       // takes and returns Integer objects
        IntUnaryOperator primitive = x -> x * 2;              // takes and returns int values

        Integer a = boxed.apply(50), b = boxed.apply(50);     // 100: inside the Integer cache
        Integer c = boxed.apply(500), d = boxed.apply(500);   // 1000: outside it
        IO.println("boxed, 100 == 100:   " + (a == b));
        IO.println("boxed, 1000 == 1000: " + (c == d));
        IO.println("boxed, equals:       " + c.equals(d));

        int e = primitive.applyAsInt(500), f = primitive.applyAsInt(500);
        IO.println("int, 1000 == 1000:   " + (e == f));
    }
}
Boxed results are objects; ints are values Python · boxing_session.py
from session import run

run("""
java Boxing.java
""")

Output

# javac 25.0.4.1
$ java Boxing.java
boxed, 100 == 100:   true
boxed, 1000 == 1000: false
boxed, equals:       true
int, 1000 == 1000:   true

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 boxing_session.py

The second line is the classic bug, and the boxing is what causes it. Integer.valueOf, which boxing uses, always reuses the cached objects for values from -128 to 127 (its Javadoc promises that range and no more), so 100 == 100 compared one cached object with itself. 1000 is outside the cache, so the two calls returned two different objects, and == compared their addresses, not their values. equals compares values. With IntUnaryOperator there is no object to compare, and no allocation either, which this measurement shows. It counts the bytes the thread allocates during a million calls of each version, keeping every result so that nothing can be optimised away:

Allocation.java Java · Allocation.java
import java.lang.management.ManagementFactory;
import java.util.function.Function;
import java.util.function.IntUnaryOperator;

class Allocation {
    /** Bytes this thread has allocated so far (HotSpot's com.sun.management extension of ThreadMXBean). */
    static long allocated() {
        return ((com.sun.management.ThreadMXBean) ManagementFactory.getThreadMXBean()).getCurrentThreadAllocatedBytes();
    }

    static void main() {
        int n = 1_000_000;
        Function<Integer, Integer> boxed = x -> x + 1000;
        IntUnaryOperator primitive = x -> x + 1000;
        Integer[] boxedResults = new Integer[n];      // the results are kept, so they must exist as objects
        int[] intResults = new int[n];
        long boxedBest = Long.MAX_VALUE, intBest = Long.MAX_VALUE;
        for (int round = 0; round < 5; round++) {     // the best of 5 rounds, once the JIT has compiled the loops
            long before = allocated();
            for (int i = 0; i < n; i++) boxedResults[i] = boxed.apply(i);
            boxedBest = Math.min(boxedBest, allocated() - before);
            before = allocated();
            for (int i = 0; i < n; i++) intResults[i] = primitive.applyAsInt(i);
            intBest = Math.min(intBest, allocated() - before);
        }
        IO.println("1,000,000 calls, best of 5 rounds:");
        IO.println("  Function<Integer, Integer>: " + boxedBest / n + " bytes per call");
        IO.println("  IntUnaryOperator:           " + intBest / n + " bytes per call");
    }
}
Bytes allocated per call, boxed and primitive Python · alloc_session.py
from session import run

run("""
java Allocation.java
""")

Output

# javac 25.0.4.1
$ java Allocation.java
1,000,000 calls, best of 5 rounds:
  Function<Integer, Integer>: 16 bytes per call
  IntUnaryOperator:           0 bytes per call

This output changes from run to run: the bytes are counted on the running JVM, whose compiler can remove some of the boxing in one run and not in another

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 alloc_session.py

The boxed version allocated about 16 bytes per call, one Integer object for each result, and the int version allocated nothing. That is the cost a profiler shows as millions of short-lived objects in a busy loop. Streams follow the same rule: IntStream.filter takes an IntPredicate, so number-heavy pipelines stay on the primitive side with mapToInt.

Composing functions and rules

Most of the interfaces carry default methods that build a new function from two existing ones, so a pipeline can be assembled from small, named, separately testable steps:

  • f.andThen(g) builds x -> g(f(x)): f, and then g.
  • f.compose(g) builds x -> f(g(x)): f, applied to what g gives.
  • p.and(q) builds x -> p(x) && q(x); q is skipped when p is false.
  • p.or(q) builds x -> p(x) || q(x); q is skipped when p is true.
  • p.negate() and Predicate.not(p) (Java 11) build x -> !p(x), the opposite rule.
  • Function.identity() is x -> x, a function that changes nothing.
Compose.java Java · Compose.java
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import java.util.function.UnaryOperator;

class Compose {
    static void main() {
        Function<Integer, Integer> plus1 = x -> x + 1;
        Function<Integer, Integer> times2 = x -> x * 2;
        IO.println("plus1.andThen(times2).apply(5) = " + plus1.andThen(times2).apply(5));   // times2(plus1(5))
        IO.println("plus1.compose(times2).apply(5) = " + plus1.compose(times2).apply(5));   // plus1(times2(5))

        // A text-cleaning pipeline: three small functions joined into one.
        UnaryOperator<String> strip = s -> s.strip();
        UnaryOperator<String> oneSpace = s -> s.replaceAll("\\s+", " ");
        UnaryOperator<String> lower = s -> s.toLowerCase(Locale.ROOT);
        Function<String, String> clean = strip.andThen(oneSpace).andThen(lower);
        for (String raw : List.of("  Hello   World ", "\tJAVA\n 25  ", "already clean")) {
            IO.println("[" + clean.apply(raw) + "]");
        }
    }
}
Applicant.java Java · Applicant.java
record Applicant(String name, int age, int marks, boolean firstGeneration) {}
Eligibility.java Java · Eligibility.java
import java.util.List;
import java.util.function.Predicate;

class Eligibility {
    static int checks = 0;

    /** Wraps a rule so that every evaluation is counted. */
    static Predicate<Applicant> counted(Predicate<Applicant> rule) {
        return a -> {
            checks++;
            return rule.test(a);
        };
    }

    static void main() {
        Predicate<Applicant> adult = counted(a -> a.age() >= 18);
        Predicate<Applicant> highMarks = counted(a -> a.marks() >= 75);
        Predicate<Applicant> relaxed = counted(a -> a.firstGeneration() && a.marks() >= 65);

        // Eligible: an adult, and either high marks or the relaxed rule.
        Predicate<Applicant> eligible = adult.and(highMarks.or(relaxed));
        Predicate<Applicant> notEligible = eligible.negate();

        List<Applicant> applicants = List.of(
                new Applicant("Asha", 19, 81, false),
                new Applicant("Ravi", 20, 68, true),
                new Applicant("Meera", 17, 92, false),
                new Applicant("Kabir", 22, 68, false));
        for (Applicant a : applicants) {
            checks = 0;
            boolean ok = eligible.test(a);
            IO.println(a.name() + ": eligible " + ok + ", rules evaluated " + checks);
        }
        IO.println("not eligible: " + applicants.stream().filter(notEligible).map(a -> a.name()).toList());
        IO.println("not adults:   " + applicants.stream().filter(Predicate.not(a -> a.age() >= 18)).map(a -> a.name()).toList());
    }
}
andThen against compose, a cleaning pipeline and composed rules Python · compose_session.py
from session import run

run("""
java Compose.java
java Eligibility.java
""")

Output

# javac 25.0.4.1
$ java Compose.java
plus1.andThen(times2).apply(5) = 12
plus1.compose(times2).apply(5) = 11
[hello world]
[java 25]
[already clean]
$ java Eligibility.java
Asha: eligible true, rules evaluated 2
Ravi: eligible true, rules evaluated 3
Meera: eligible false, rules evaluated 1
Kabir: eligible false, rules evaluated 3
not eligible: [Meera, Kabir]
not adults:   [Meera]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 compose_session.py

andThen and compose differ only in order, and the numbers show it: (5 + 1) * 2 = 12 against 5 * 2 + 1 = 11. The cleaning pipeline joins three one-line steps into clean, which turns " Hello World " into "hello world". Each step can be tested alone, and reordering them is a one-word change. The regular expression \s+ matches any run of whitespace, tabs and line breaks included.

Regex Tester Try the \s+ pattern from the cleaning step on your own text and see every run of whitespace it matches.

The eligibility rule adult.and(highMarks.or(relaxed)) reads like the policy it implements, and the counts show the short-circuiting that the Predicate Javadoc promises. Meera, at 17, failed adult, so neither marks rule ran: 1 evaluation. Asha passed adult and highMarks, so relaxed was skipped: 2. Ravi and Kabir needed all three. Order your rules so that the cheap or most selective one comes first, and so that a rule that would fail on bad data, such as one that reads a field that may be null, comes after the rule that rejects that data.

Writing your own functional interface

Write your own interface when no standard shape fits: a domain name that says more than Function (Discount, TaxRule), three or more parameters, or a method that must throw a checked exception. Mark it @FunctionalInterface. The annotation is optional, because the compiler treats any interface with one abstract method as functional, but with it javac refuses the interface the moment someone adds a second abstract method:

TwoMethods.java Java · TwoMethods.java
@FunctionalInterface
interface Discount {
    int apply(int price);
    int applyTwice(int price);
}

class TwoMethods {
    static void main() {
        Discount half = p -> p / 2;
    }
}
Discounts.java Java · Discounts.java
@FunctionalInterface
interface Discount {
    int apply(int price);                          // the one abstract method

    default Discount then(Discount next) {         // default methods do not count
        return price -> next.apply(apply(price));
    }

    static Discount percent(int percent) {         // nor do static methods
        return price -> price - price * percent / 100;
    }

    boolean equals(Object other);                  // nor do public methods of Object
}

class Discounts {
    static void main() {
        Discount festival = Discount.percent(10).then(price -> price - 50);
        IO.println(festival.apply(1000));        // 1000 - 100 = 900, then - 50
        IO.println(festival.apply(400));         // 400 - 40 = 360, then - 50
    }
}
A second abstract method, refused; default and static methods, allowed Python · own_session.py
from session import run

run("""
java TwoMethods.java
java Discounts.java
""", fails=("java TwoMethods.java",))

Output

# javac 25.0.4.1
$ java TwoMethods.java
TwoMethods.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
  Discount is not a functional interface
    multiple non-overriding abstract methods found in interface Discount
TwoMethods.java:9: error: incompatible types: Discount is not a functional interface
        Discount half = p -> p / 2;
                        ^
    multiple non-overriding abstract methods found in interface Discount
2 errors
error: compilation failed
[exit status 1]
$ java Discounts.java
850
310

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 own_session.py

Discounts.java shows what does not count towards the one abstract method. Default methods have a body. Static methods belong to the interface, not to its objects. An abstract method with the signature of a public Object method, here equals, is already implemented by every object (JLS §9.8). That is why java.util.Comparator, which declares equals, is still a functional interface. The default method then lets discounts chain the way andThen chains functions: 10 % off 1,000 is 900, and 50 off that is 850.

Checked exceptions

Function.apply declares no checked exceptions, so a lambda that calls a method throwing IOException cannot be a Function:

ReadError.java Java · ReadError.java
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

class ReadError {
    static void main() {
        List<Path> files = List.of(Path.of("data/class-a.txt"), Path.of("data/class-b.txt"));
        List<String> texts = files.stream().map(p -> Files.readString(p)).toList();
        IO.println(texts);
    }
}
ReadAll.java Java · ReadAll.java
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Function;

/** Like Function, but apply may throw an IOException. */
@FunctionalInterface
interface IOFunction<T, R> {
    R apply(T t) throws IOException;

    /** A Function that rethrows the IOException as an UncheckedIOException. */
    static <T, R> Function<T, R> unchecked(IOFunction<T, R> f) {
        return t -> {
            try {
                return f.apply(t);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        };
    }
}

class ReadAll {
    static List<String> lines(List<Path> files) {
        return files.stream()
                .map(IOFunction.unchecked(p -> Files.readString(p)))
                .flatMap(text -> text.lines())
                .toList();
    }

    static void main() {
        IO.println(lines(List.of(Path.of("data/class-a.txt"), Path.of("data/class-b.txt"))));
        try {
            lines(List.of(Path.of("data/class-a.txt"), Path.of("data/class-c.txt")));
        } catch (UncheckedIOException e) {
            IO.println("failed: " + e.getCause().getClass().getSimpleName() + ": " + e.getCause().getMessage());
        }
    }
}
The compile error, and a function type that declares IOException Python · checked_session.py
from session import run

run("""
java ReadError.java
java ReadAll.java
""", fails=("java ReadError.java",))

Output

# javac 25.0.4.1
$ java ReadError.java
ReadError.java:8: error: unreported exception IOException; must be caught or declared to be thrown
        List<String> texts = files.stream().map(p -> Files.readString(p)).toList();
                                                                     ^
1 error
error: compilation failed
[exit status 1]
$ java ReadAll.java
[Asha 91, Ravi 82, Meera 88]
failed: NoSuchFileException: data/class-c.txt

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 checked_session.py

IOFunction<T, R> has the shape of Function, with throws IOException on its apply. Its static method unchecked adapts one to a plain Function by rethrowing the exception wrapped in UncheckedIOException, the JDK’s own unchecked wrapper for I/O errors, so the cause stays available to whoever catches it. The second call shows the trade-off: the stream stops at the first file that fails, here the missing class-c.txt. When you need a report of every failure rather than the first one, return a result for each element instead of throwing, which the interview questions of this lesson come back to.

Your turn: a rule engine from a configuration

Build fromConfig(List<String> names, Map<String, Predicate<Applicant>> rules). It looks up each named rule and joins them with and, in the order of the configuration, so that a call stops at the first rule that fails. An empty configuration lets everyone through, and an unknown name throws an IllegalArgumentException that names it. The checks in main record which rules ran, so they test the order and the short-circuiting, not only the answers.

RuleEngine.java: one solution, with its checks Java · RuleEngine.java
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;

class RuleEngine {
    /** Joins the rules named in the configuration with and(), in the order the configuration lists them. */
    static Predicate<Applicant> fromConfig(List<String> names, Map<String, Predicate<Applicant>> rules) {
        Predicate<Applicant> all = a -> true;              // no rules: everyone passes
        for (String name : names) {
            Predicate<Applicant> rule = rules.get(name);
            if (rule == null) throw new IllegalArgumentException("unknown rule: " + name);
            all = all.and(rule);
        }
        return all;
    }

    static final List<String> log = new ArrayList<>();

    /** A rule that records its name each time it runs, so the checks can see the order. */
    static Predicate<Applicant> logged(String name, Predicate<Applicant> rule) {
        return a -> {
            log.add(name);
            return rule.test(a);
        };
    }

    static void check(String what, Object got, Object want) {
        IO.println((got.equals(want) ? "ok    " : "WRONG ") + what + ": " + got);
    }

    static void main() {
        Map<String, Predicate<Applicant>> rules = Map.of(
                "adult", logged("adult", a -> a.age() >= 18),
                "marks", logged("marks", a -> a.marks() >= 75),
                "first-generation", logged("first-generation", a -> a.firstGeneration()));
        Applicant asha = new Applicant("Asha", 19, 81, false);
        Applicant meera = new Applicant("Meera", 17, 92, true);

        check("adult and marks", fromConfig(List.of("adult", "marks"), rules).test(asha), true);
        log.clear();
        fromConfig(List.of("adult", "marks", "first-generation"), rules).test(meera);
        check("stops at the first failing rule", log, List.of("adult"));
        log.clear();
        fromConfig(List.of("marks", "adult"), rules).test(meera);
        check("runs in the configured order", log, List.of("marks", "adult"));
        check("no rules", fromConfig(List.of(), rules).test(meera), true);
        try {
            fromConfig(List.of("adult", "income"), rules);
            check("unknown rule", "no exception", "an exception");
        } catch (IllegalArgumentException e) {
            check("unknown rule", e.getMessage(), "unknown rule: income");
        }
    }
}
The checks Python · exercise_session.py
from session import run

run("""
java RuleEngine.java
""")

Output

# javac 25.0.4.1
$ java RuleEngine.java
ok    adult and marks: true
ok    stops at the first failing rule: [adult]
ok    runs in the configured order: [marks, adult]
ok    no rules: true
ok    unknown rule: unknown rule: income

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exercise_session.py

Starting from a -> true gives the empty configuration its meaning: and with nothing added keeps everyone. The loop could also be written as a stream, names.stream().map(…).reduce(a -> true, Predicate::and), but the loop makes the unknown-name check easy to place and read.

Interview questions

Fresher · What makes an interface a functional interface, and is the @FunctionalInterface annotation required?

A functional interface has exactly one abstract method, so a lambda or a method reference can implement it. Default and static methods do not count, and neither does an abstract method that matches a public method of Object, such as equals, because every object already implements it; that is why Comparator is functional. The annotation is not required: javac treats any interface that meets the rule as functional. Adding @FunctionalInterface documents the intent and makes the compiler reject the interface as soon as someone adds a second abstract method, instead of breaking every lambda that implements it later. Before writing your own, check java.util.function: its 43 interfaces cover the common shapes, including primitive versions such as IntPredicate.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A profile of a pricing service shows millions of Integer objects created per second inside a loop that applies a chain of Function<Integer, Integer> discount rules to prices. What would you change, and how would you show that the change helped?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A nightly job reads 10,000 files with paths.stream().map(p -> Files.readString(p)), which does not compile. Compare three ways to make it work, and choose one for a job that must report every file it could not read.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Four shapes cover most needs: Function (in and out), Predicate (in, true or false), Consumer (in, nothing out) and Supplier (nothing in, value out). UnaryOperator and BinaryOperator fix the result type to the input.
  • The other names follow rules: Bi for two inputs, Int, Long and Double for primitive inputs, To… for a primitive result, Obj for an object before a primitive. JDK 25 has 43 such interfaces.
  • Primitive versions avoid boxing. Boxed results are objects, so == on them is a bug outside the cached range of -128 to 127.
  • andThen runs left to right and compose right to left. and and or short-circuit, so put cheap and guarding rules first.
  • Write your own interface for a domain name, more parameters or checked exceptions, and mark it @FunctionalInterface so that javac guards the single abstract method.

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 A method needs a rule that takes a String and answers yes or no. Which type fits best?

    Choose one answer.

    Show the answer to question 1

    Answer: Predicate<String>

    Predicate<String> has boolean test(String), which returns a primitive boolean and brings and, or and negate with it. Function<String, Boolean> would also compile, but it returns a boxed Boolean and has no predicate methods. A Supplier takes nothing and a Consumer returns nothing.

  2. Question 2 of 5 With Function<Integer, Integer> plus1 = x -> x + 1 and times2 = x -> x * 2, what does plus1.compose(times2).apply(3) return?

    Type a number.

    Show the answer to question 2

    Answer: 7

    compose runs its argument first: plus1(times2(3)) = 3 * 2 + 1 = 7. plus1.andThen(times2).apply(3) would run plus1 first and give (3 + 1) * 2 = 8. The Compose session printed 11 and 12 for the same pair applied to 5.

  3. Question 3 of 5 What does this print?

    Read the code, then choose one answer.

    Function<Integer, Integer> twice = x -> x * 2;
    IO.println((twice.apply(50) == twice.apply(50)) + " " + (twice.apply(500) == twice.apply(500)));
    Show the answer to question 3

    Answer: true false

    Each result is boxed with Integer.valueOf, which always reuses objects for values from -128 to 127. 100 comes from that cache, so both calls return the same object; 1000 does not, so == compares two different objects. Compare boxed values with equals, or use IntUnaryOperator and keep plain ints.

  4. Question 4 of 5 Which of these interfaces is not a functional interface?

    Choose one answer.

    Show the answer to question 4

    Answer: interface A { int apply(int x); int applyTwice(int x); }

    A has two abstract methods. Default and static methods do not count, and neither does an abstract method that matches a public method of Object, such as equals, because every object already implements it. That is why Comparator, which declares equals, is still a functional interface.

  5. Question 5 of 5 Why does files.stream().map(p -> Files.readString(p)) fail to compile?

    Choose one answer.

    Show the answer to question 5

    Answer: Function.apply declares no checked exceptions, and Files.readString can throw IOException.

    A lambda's body may only throw the checked exceptions its interface method declares, and Function.apply declares none. Catch the exception inside the lambda, or adapt a function type that declares it, as IOFunction in the lesson does, rethrowing it as an UncheckedIOException.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.