Checked Interfaces
Runnable example:
CheckedInterfacesSample.java
The problem: checked exceptions in lambdas
Java’s standard functional interfaces — Function, Supplier, Consumer,
Runnable — do not declare checked exceptions. Any lambda that calls a method
throwing a checked exception must wrap it in a try/catch block, converting
it to an unchecked exception inline. This produces noise that obscures intent.
// Standard Function<T,R> does not declare checked exceptions.// Every checked call inside a lambda requires manual wrapping.Function<Path, String> reader = path -> { try { return Files.readString(path); // IOException is checked } catch (IOException e) { throw new RuntimeException(e); // boilerplate every time }};
// With CheckedFunction the method reference is used directly:CheckedFunction<Path, String> reader = Files::readString; // cleandmx-fun provides Checked* variants of the four most common functional interfaces
so that throwing method references can be used directly.
Checked functional interfaces
| Interface | Checked equivalent of | Signature |
|---|---|---|
CheckedFunction<T, R> | Function<T, R> | R apply(T t) throws Exception |
CheckedSupplier<T> | Supplier<T> | T get() throws Exception |
CheckedConsumer<T> | Consumer<T> | void accept(T t) throws Exception |
CheckedRunnable | Runnable | void run() throws Exception |
All four are @FunctionalInterface and @NullMarked.
// CheckedFunction<T, R> — like Function<T,R> but may throw ExceptionCheckedFunction<Path, String> readFile = Files::readString;CheckedFunction<String, Class<?>> loader = Class::forName;
// CheckedSupplier<T> — like Supplier<T> but may throw ExceptionCheckedSupplier<Connection> openConn = () -> dataSource.getConnection();
// CheckedConsumer<T> — like Consumer<T> but may throw ExceptionCheckedConsumer<OutputStream> closeStream = Closeable::close;
// CheckedRunnable — like Runnable but may throw ExceptionCheckedRunnable initDb = () -> flyway.migrate();Integrating with Try
CheckedSupplier<T> is the natural input for Try.of() — the supplier is
executed and any thrown exception is captured as a Failure without any
manual wrapping. CheckedRunnable feeds Try.run() for side-effecting
operations that produce no value.
// Try.of(CheckedSupplier<V>) — captures any exception as FailureTry<String> content = Try.of(() -> Files.readString(path));
// Method reference — Files::readString matches CheckedFunction<Path,String>// but also satisfies CheckedSupplier<String> when path is already in scopeTry<String> content2 = Try.of(() -> Files.readString(Paths.get("config.txt")));
// Try.run(CheckedRunnable) — for side-effecting operations that return nothingTry<Void> migration = Try.run(() -> flyway.migrate());
// CheckedFunction used with Try.traverse — apply to every element,// collecting results or accumulating the first failureList<Path> paths = List.of(Paths.get("a.txt"), Paths.get("b.txt"));
Try<List<String>> allContents = Try.traverse(paths, path -> Try.of(() -> Files.readString(path)));// Success([...]) if all reads succeed, Failure(IOException) on first errorHigher-arity function interfaces
The standard library stops at BiFunction<A,B,R>. dmx-fun extends this with:
| Interface | Arguments | Used by |
|---|---|---|
TriFunction<A, B, C, R> | 3 | Tuple3.map(TriFunction) |
QuadFunction<A, B, C, D, R> | 4 | Tuple4.map(QuadFunction) |
Both follow the same @FunctionalInterface + @NullMarked contract as the
standard library interfaces.
// TriFunction<A, B, C, R> — three arguments, one resultTriFunction<String, Integer, Boolean, String> describe = (name, age, active) -> name + " (age " + age + ", active=" + active + ")";
String label = describe.apply("Alice", 30, true);// "Alice (age 30, active=true)"
// QuadFunction<A, B, C, D, R> — four arguments, one resultQuadFunction<String, Integer, Boolean, Double, String> summarize = (name, age, active, score) -> name + " | " + age + "y | " + (active ? "active" : "inactive") + " | " + score;
String summary = summarize.apply("Alice", 30, true, 9.5);// "Alice | 30y | active | 9.5"
// Both are used by Tuple3.map and Tuple4.mapString t3result = Tuple3.of("Alice", 30, true).map(describe);String t4result = Tuple4.of("Alice", 30, true, 9.5).map(summarize);When to use each
| Scenario | Interface |
|---|---|
| Pass a throwing method reference to a functional API | CheckedFunction / CheckedSupplier |
| Wrap a throwing void action (e.g. DB migration, file write) | CheckedRunnable |
| Side-effecting loop body that throws | CheckedConsumer |
Feed a throwing supplier into Try.of() | CheckedSupplier |
Collapse a Tuple3 into a single value | TriFunction |
Collapse a Tuple4 into a single value | QuadFunction |
Combine more than two values in a Validated.combine chain | TriFunction / QuadFunction |
Real-world example
Reading and parsing a set of report files using CheckedFunction, Try.traverse,
and TriFunction to keep the pipeline clean and exception-safe.
// Read a set of report files and parse each one, collecting results or failures.// CheckedFunction keeps the lambda clean; Try.traverse accumulates the outcome.
List<Path> reportPaths = List.of( Paths.get("reports/q1.csv"), Paths.get("reports/q2.csv"), Paths.get("reports/q3.csv"));
// Step 1 — read each file; any IOException becomes a FailureTry<List<String>> rawContents = Try.traverse(reportPaths, path -> Try.of(() -> Files.readString(path)));
// Step 2 — parse each raw string into a Report; parse errors become FailuresTry<List<Report>> reports = rawContents.flatMap(contents -> Try.traverse(contents, raw -> Try.of(() -> Report.parse(raw))));
// Step 3 — generate the combined output or log the first errorswitch (reports) { case Try.Success<List<Report>>(var list) -> exportCombinedReport(list); case Try.Failure<List<Report>>(var ex) -> log.error("Report generation failed: {}", ex.getMessage(), ex);}
// TriFunction used to merge three quarterly totals into a summary recordTriFunction<BigDecimal, BigDecimal, BigDecimal, YearlySummary> toSummary = (q1, q2, q3) -> new YearlySummary(q1.add(q2).add(q3));
YearlySummary yearly = Tuple3.of(q1Total, q2Total, q3Total).map(toSummary);