javaexceptioninterfacemapping

Adapt exception type of generic throwing iterator


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 Throwables and check if they're of the expected type using a class object, but that feels clunky.


Solution

  • 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 NoSuchElementExceptions 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.