Challenge

Get a checked exception through a stream

20 minintermediate28 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 Starter

Hints

  1. Hint 1

    Look at the signature of Function.apply. There is no throws clause, and you cannot add one.

  2. Hint 2

    You can declare your own interface whose method throws Exception, then write an adapter that turns it into a Function.

  3. Hint 3

    The adapter has to do something with the checked exception. Whatever it does IS the design decision — name it.

  4. 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?