ExerciseChallenge
Challenge
Get a checked exception through a stream
20 minintermediate2–8 yrs
Edge cases. You have to reason, and two valid fixes differ.
What this teaches
- No standard functional interface declares throws, so checked exceptions cannot escape a lambda
- Every workaround converts checked to unchecked at the lambda boundary
- Wrapping loses the type; a result object keeps it but changes the pipeline
- This constraint, not an opinion, is why modern APIs throw unchecked
Starter
Starter.java
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
/**
* Challenge: the language will not let you do the obvious thing.
*
* parse() throws a checked exception. Try to call it from inside a map() and
* the code does not compile — Function.apply declares no throws clause, and you
* cannot change that.
*
* Solve it twice, and decide which one you would ship.
*/
public class Starter {
/** Stands in for anything realistic: Files.readString, JDBC, a parser. */
static int parse(String s) throws ParseFailure {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
throw new ParseFailure("not a number: " + s, e);
}
}
/** Checked on purpose — that is the whole exercise. */
static class ParseFailure extends Exception {
ParseFailure(String message, Throwable cause) {
super(message, cause);
}
}
public static void main(String[] args) {
List<String> input = List.of("1", "2", "oops", "4");
// This is what you want to write. Uncomment it and read the error.
//
// List<Integer> parsed = input.stream()
// .map(s -> parse(s))
// .toList();
//
// error: unreported exception ParseFailure; must be caught or declared
// The loop version compiles fine, which tells you the restriction is
// about lambdas, not about streams.
List<Integer> viaLoop = new ArrayList<>();
int failures = 0;
for (String s : input) {
try {
viaLoop.add(parse(s));
} catch (ParseFailure e) {
failures++;
}
}
System.out.println("loop parsed = " + viaLoop + ", failures = " + failures);
// TODO 1: declare a functional interface whose method is allowed to
// throw:
// @FunctionalInterface interface ThrowingFunction<T, R> {
// R apply(T t) throws Exception;
// }
// Then write a static adapter:
// static <T, R> Function<T, R> unchecked(ThrowingFunction<T, R> f)
// that returns a normal Function. Use it to make the stream compile.
//
// The adapter must decide what to do with the checked exception. That
// decision is the answer to this exercise — write it down.
// TODO 2: solve it a second way without throwing at all. Make the
// mapper return a small result type — say a record holding either the
// parsed value or the failure — so the pipeline stays a pure stream and
// the caller partitions successes from failures at the end.
//
// Hint: Collectors.partitioningBy.
// TODO 3: one comment each. What does approach 1 cost? What does
// approach 2 cost? Which one would you put in a shared library, and
// which in application code?
}
}Run it locally:
cd exercises/java/exceptions/checked-vs-unchecked-exceptions/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterHints
Hint 1
Look at the signature of Function.apply. There is no throws clause, and you cannot add one.
Hint 2
You can declare your own interface whose method throws Exception, then write an adapter that turns it into a Function.
Hint 3
The adapter has to do something with the checked exception. Whatever it does IS the design decision — name it.
Hint 4
The second approach does not throw at all: make the mapper return a value that holds either the result or the failure.
Done when
- The stream compiles and processes every element
- A failure on one element does not silently vanish
- Both approaches implemented, with a comment on what each one costs
← Back to What is the difference between checked and unchecked exceptions?