I have an interface ThrowingIterator
, which follows the general contract of Iterator
, except the hasNext()
and next()
methods can throw exceptions:
public interface ThrowingIterator<T, E extends Throwable> {
boolean hasNext() throws E;
T next() throws E;
default void remove() throws E { /* throw unsupported */ }
// forEachRemaining same as Iterator
}
I can change the return type of the iterator by using an adapt function, similar to how Stream
has map(Function<? super T, U> mapper)
. However, I haven't been able to figure out a way to change the exception type of the iterator, like the following:
// example method
default <X extends Throwable> ThrowingIterator<T, X> adaptException(Function<? super E, ? extends X> exceptionMapper) {
return new ThrowingIterator<T, X> {
public boolean hasNext() {
try {
return this.hasNext();
} catch (E e) { // this does not work, can't catch E
throw exceptionMapper.apply(e);
}
}
}
// same for next()
}
// example use
ThrowingIterator<Integer, IOException> baseIterator = getIterator();
ThrowingIterator<Integer, ExecutionException> adaptedIterator = baseIterator.adaptException(ExecutionException::new);
My main difficulty in writing this function comes from the fact that Java does not allow a generic exception type to be caught. Is there any way around this restriction? I could catch all Throwable
s and check if they're of the expected type using a class object, but that feels clunky.
I found a solution that satisfied what I wanted:
@SuppressWarnings("unchecked")
public static <T, E extends Exception, X extends Throwable> ThrowingIterator<T, X> adaptException(ThrowingIterator<T, E> iter, Class<E> exceptionType, Function<? super E, ? extends X> exceptionMappingFunction) {
if (NoSuchElementException.class.isAssignableFrom(exceptionType)
|| RuntimeException.class.equals(exceptionType)
|| Exception.class.equals(exceptionType)) {
throw new IllegalArgumentException("Applying this method to an iterator that is typed to throw a superclass of NoSuchElementException is a Very Bad Idea. "
+ "The returned iterator would have no way to communicate if such an exception thrown was because the source iterator ran out of elements, or because "
+ "the data source threw an exception. This method should only be used on iterators that throw checked exceptions, as a way to add additional "
+ "context to an exception that came from the iterator's data source, although other unchecked exceptions are permissible");
}
return new ThrowingIterator<T, X>() {
@Override
public boolean hasNext() throws X {
try {
return iter.hasNext();
} catch (RuntimeException e) {
// Unchecked cast warning suppressed since we check if it's an instance of E
if (exceptionType.isInstance(e)) throw exceptionMappingFunction.apply((E) e);
// allows NoSuchElementException to pass through normally
throw e;
} catch (Exception e) {
// Should never have a ClassCastException since the iterator can only throw E or a RuntimeException subclass
throw exceptionMappingFunction.apply((E) e);
}
}
@Override
public T next() throws X {
try {
return iter.next();
} catch (RuntimeException e) {
if (exceptionType.isInstance(e)) throw exceptionMappingFunction.apply((E) e);
throw e;
} catch (Exception e) {
throw exceptionMappingFunction.apply((E) e);
}
}
};
}
The big IllegalArgumentException
check at the top is necessary because there's no way for me to wildcard <E extends Exception && !(E super NoSuchElementException)>
, since I want all NoSuchElementException
s to continue to be thrown upward without having the mapping function applied. I also didn't use <E extends Throwable>
because I especially didn't want to interrupt an Error
being thrown up the call stack.