Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn't Java allow generic subclasses of Throwable?

People also ask

Why does Java not allow generic exception classes?

It's essentially because it was designed in a bad way. The fact that a catch clause would fail for generics are not reified is no excuse for that. The compiler could simply disallow concrete generic types that extend Throwable or disallow generics inside catch clauses.

What are throwable exceptions in Java?

The Throwable class is the superclass of all errors and exceptions in the Java language. Only objects that are instances of this class (or one of its subclasses) are thrown by the Java Virtual Machine or can be thrown by the Java throw statement.

Why throwable is a class not interface?

James Gosling: The reason that the Throwable and the rest of those guys are not interfaces is because we decided, or I decided fairly early on. I decided that I wanted to have some state associated with every exception that gets thrown. And you can't do that with interfaces; you can only do that with classes.

Can I use polymorphism in generics?

Using Java Generic concept, we might write a generic method for sorting an array of objects, then invoke the generic method with Integer arrays, Double arrays, String arrays and so on, to sort the array elements. Polymorphism is the ability of an object to take on many forms.


As mark said, the types are not reifiable, which is a problem in the following case:

try {
   doSomeStuff();
} catch (SomeException<Integer> e) {
   // ignore that
} catch (SomeException<String> e) {
   crashAndBurn()
}

Both SomeException<Integer> and SomeException<String> are erased to the same type, there is no way for the JVM to distinguish the exception instances, and therefore no way to tell which catch block should be executed.


Here is a simple example of how to use the exception:

class IntegerExceptionTest {
  public static void main(String[] args) {
    try {
      throw new IntegerException(42);
    } catch (IntegerException e) {
      assert e.getValue() == 42;
    }
  }
}

The body of the TRy statement throws the exception with a given value, which is caught by the catch clause.

In contrast, the following definition of a new exception is prohibited, because it creates a parameterized type:

class ParametricException<T> extends Exception {  // compile-time error
  private final T value;
  public ParametricException(T value) { this.value = value; }
  public T getValue() { return value; }
}

An attempt to compile the above reports an error:

% javac ParametricException.java
ParametricException.java:1: a generic class may not extend
java.lang.Throwable
class ParametricException<T> extends Exception {  // compile-time error
                                     ^
1 error

This restriction is sensible because almost any attempt to catch such an exception must fail, because the type is not reifiable. One might expect a typical use of the exception to be something like the following:

class ParametricExceptionTest {
  public static void main(String[] args) {
    try {
      throw new ParametricException<Integer>(42);
    } catch (ParametricException<Integer> e) {  // compile-time error
      assert e.getValue()==42;
    }
  }
}

This is not permitted, because the type in the catch clause is not reifiable. At the time of this writing, the Sun compiler reports a cascade of syntax errors in such a case:

% javac ParametricExceptionTest.java
ParametricExceptionTest.java:5: <identifier> expected
    } catch (ParametricException<Integer> e) {
                                ^
ParametricExceptionTest.java:8: ')' expected
  }
  ^
ParametricExceptionTest.java:9: '}' expected
}
 ^
3 errors

Because exceptions cannot be parametric, the syntax is restricted so that the type must be written as an identifier, with no following parameter.


It's essentially because it was designed in a bad way.

This issue prevents clean abstract design e.g.,

public interface Repository<ID, E extends Entity<ID>> {

    E getById(ID id) throws EntityNotFoundException<E, ID>;
}

The fact that a catch clause would fail for generics are not reified is no excuse for that. The compiler could simply disallow concrete generic types that extend Throwable or disallow generics inside catch clauses.


Generics are checked at compile-time for type-correctness. The generic type information is then removed in a process called type erasure. For example, List<Integer> will be converted to the non-generic type List.

Because of type erasure, type parameters cannot be determined at run-time.

Let's assume you are allowed to extend Throwable like this:

public class GenericException<T> extends Throwable

Now let's consider the following code:

try {
    throw new GenericException<Integer>();
}
catch(GenericException<Integer> e) {
    System.err.println("Integer");
}
catch(GenericException<String> e) {
    System.err.println("String");
}

Due to type erasure, the runtime will not know which catch block to execute.

Therefore it is a compile-time error if a generic class is a direct or indirect subclass of Throwable.

Source: Problems with type erasure


I would expect that it's because there's no way to guarantee the parameterization. Consider the following code:

try
{
    doSomethingThatCanThrow();
}
catch (MyException<Foo> e)
{
    // handle it
}

As you note, parameterization is just syntactic sugar. However, the compiler tries to ensure that parameterization remains consistent across all references to an object in compilation scope. In the case of an exception, the compiler has no way to guarantee that MyException is only thrown from a scope that it is processing.


Not too related to the question but if you really want to have an inner class that extends a Throwable you can declare it static. This is applicable when the Throwable is logically related to the enclosing class but not to the specific generic type of that enclosing class. By declaring it static, it isn't bound to an instance of the enclosing class therefore the problem disappears.

The following (admittedly not very good) example illustrates this:

/** A map for <String, V> pairs where the Vs must be strictly increasing */
public class IncreasingPairs<V extends Comparable<V>> {

    private final Map<String, V> map;

    public IncreasingPairs() {
        map = new HashMap<>();
    }

    public void insertPair(String newKey, V value) {
        // ensure new value is bigger than every value already in the map
        for (String oldKey : map.keySet())
            if (!(value.compareTo(map.get(oldKey)) > 0))
                throw new InvalidPairException(newKey, oldKey);

        map.put(newKey, value);
    }

    /** Thrown when an invalid Pair is inserted */
    public static class InvalidPairException extends RuntimeException {

        /** Constructs the Exception, independent of V! */
        public InvalidPairException(String newKey, String oldKey) {
            super(String.format("Value with key %s is not bigger than the value associated with existing key %s",
                    newKey, oldKey));
        }
    }
}

Further reading: docs.oracle.com