How it works
Java has two types of exceptions: checked and unchecked. If you want to throw a checked exception, you have to say so in your method signature. You're allowed to throw an unchecked exception whenever you want.
This library works by exploiting this system. It catches checked exceptions and rethrows them as unchecked exceptions so that the method signature is compatible with that of java.util.stream.Stream
. Then, in a terminal operation, it catches the unchecked exception and rethrows it as the checked exception.
Here's a simplified example:
class StreamAdapter<T, X extends Throwable> implements ThrowingStream<T, X> {
private static class AdapterException extends RuntimeException {
public AdapterException(Throwable cause) {
super(cause);
}
}
private final Stream<T> delegate;
private final Class<X> x;
StreamAdapter(Stream<T> delegate, Class<X> x) {
this.delegate = delegate;
this.x = x;
}
private <R> R maskException(ThrowingSupplier<R, X> method) {
try {
return method.get();
} catch (Throwable t) {
if (x.isInstance(t)) {
throw new AdapterException(t);
} else {
throw t;
}
}
}
@Override
public ThrowingStream<T, X> filter(ThrowingPredicate<T, X> predicate) {
return new StreamAdapter<>(
delegate.filter(t -> maskException(() -> predicate.test(t))), x);
}
@Override
public <R> ThrowingStream<R, X> map(ThrowingFunction<T, R, X> mapper) {
return new StreamAdapter<>(
delegate.map(t -> maskException(() -> mapper.apply(t))), x);
}
private <R> R unmaskException(Supplier<R> method) throws X {
try {
return method.get();
} catch (AdapterException e) {
throw x.cast(e.getCause());
}
}
@Override
public <A, R> R collect(Collector<T, A, R> collector) throws X {
return unmaskException(() -> delegate.collect(collector));
}
}
The two important methods here are maskException
and unmaskException
. maskException
catches a checked exception and masks it up by wrapping it in a AdapterException
. unmaskException
does the opposite: it catches AdapterExceptions
and rethrows the original, generic exception.