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.
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
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>, methodR apply(T t): a value in, a value out.mapin a stream takes one, and so does any conversion from one type to another.Predicate<T>, methodboolean test(T t): a value in, true or false out.filtertakes one, and validation rules are predicates.Consumer<T>, methodvoid accept(T t): a value in, nothing out.forEachtakes one, as do logging and writing.Supplier<T>, methodT get(): nothing in, a value out. Defaults made only when needed and factories are suppliers.UnaryOperator<T>, methodT apply(T t): aFunction<T, T>.List.replaceAlltakes one, and so do text clean-up steps.BinaryOperator<T>, methodT apply(T a, T b): aBiFunction<T, T, T>.reducetakes 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:
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 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>.
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:
Bimeans two inputs:BiFunction<T, U, R>,BiPredicate<T, U>,BiConsumer<T, U>.Int,Long,Doubleat the start mean primitive inputs:IntPredicatetests anint.To…means a primitive result:ToIntFunction<T>turns aTinto anint, andIntToLongFunctionanintinto along.Objkeeps the first input an object:ObjIntConsumer<T>accepts aTand anint.
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:
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));
}
} 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:
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");
}
} 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)buildsx -> g(f(x)): f, and then g.f.compose(g)buildsx -> f(g(x)): f, applied to what g gives.p.and(q)buildsx -> p(x) && q(x);qis skipped whenpis false.p.or(q)buildsx -> p(x) || q(x);qis skipped whenpis true.p.negate()andPredicate.not(p)(Java 11) buildx -> !p(x), the opposite rule.Function.identity()isx -> x, a function that changes nothing.
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) + "]");
}
}
} record Applicant(String name, int age, int marks, boolean firstGeneration) {} 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());
}
} 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.
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:
@FunctionalInterface
interface Discount {
int apply(int price);
int applyTwice(int price);
}
class TwoMethods {
static void main() {
Discount half = p -> p / 2;
}
} @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
}
} 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:
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);
}
} 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());
}
}
} 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.
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");
}
}
} 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.
-
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?
-
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.
Key takeaways
- Four shapes cover most needs:
Function(in and out),Predicate(in, true or false),Consumer(in, nothing out) andSupplier(nothing in, value out).UnaryOperatorandBinaryOperatorfix the result type to the input. - The other names follow rules:
Bifor two inputs,Int,LongandDoublefor primitive inputs,To…for a primitive result,Objfor 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. andThenruns left to right andcomposeright to left.andandorshort-circuit, so put cheap and guarding rules first.- Write your own interface for a domain name, more parameters or checked exceptions, and mark it
@FunctionalInterfaceso thatjavacguards 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.
References
- Package java.util.function (Java SE 25) (Oracle)
- FunctionalInterface (Java SE 25) (Oracle)
- Function (Java SE 25) (Oracle)
- Predicate (Java SE 25) (Oracle)
- The Java Language Specification, Java SE 25 Edition: 9.8 Functional Interfaces (Oracle)
- Integer (Java SE 25): valueOf(int) (Oracle)
- UncheckedIOException (Java SE 25) (Oracle)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress