182

According to the Java Language Sepecification, 3rd edition:

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

I wish to understand why this decision has been made. What's wrong with generic exceptions?

(As far as I know, generics are simply compile-time syntactic sugar, and they will be translated to Object anyway in the .class files, so effectively declaring a generic class is as if everything in it was an Object. Please correct me if I'm wrong.)

Hosam Aly
  • 41,555
  • 36
  • 141
  • 182
  • 2
    Generic type arguments are replaced by the upper bound, which by default is Object. If you have something like List extends A>, then A is used in the class files. – Torsten Marek Feb 01 '09 at 18:12
  • Thank you @Torsten. I didn't think of that case before. – Hosam Aly Feb 01 '09 at 18:24
  • 2
    It's a good interview question, this one. – skaffman Jul 07 '09 at 13:57
  • @TorstenMarek: If one calls `myList.get(i)`, obviously `get` still returns an `Object`. Does the compiler insert a cast to `A` in order to capture some of the constraint at runtime? If not, the OP is right that in the end it boils down to `Object`s at runtime. (The class file certainly contains metadata about `A`, but it's only metadata AFAIK.) – Mihai Danila Jan 19 '14 at 04:51

6 Answers6

181

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.

Torsten Marek
  • 83,780
  • 21
  • 91
  • 98
  • 8
    but what does "reifiable" mean? – aberrant80 Aug 17 '09 at 08:50
  • 1
    @aberrant80: simply speaking it means that you can get to the concret type at runtime. Which is not the fact in Java. – Joachim Sauer Aug 17 '09 at 08:57
  • 85
    So the rule should not be "generic types cannot subclass Throwable" but instead "catch clauses must always use raw types". – Archie Apr 19 '11 at 16:39
  • 3
    They could just disallow using two catch blocks with the same type together. So that using SomeExc alone would be legal, only using SomeExc and SomeExc together would be illegal. That would make no problems, or would it? – Viliam Búr Feb 14 '13 at 13:03
  • 4
    Oh, now I get it. My solution would cause problems with RuntimeExceptions, which don't have to be declared. So if SomeExc is a subclass of RuntimeException, I could throw and explicitly catch SomeExc, but maybe some other function is silently throwing SomeExc and my catch block for SomeExc would accidentally catch that too. – Viliam Búr Feb 14 '13 at 13:08
  • @ViliamBúr: nonetheless, the same risk is present today with things like `process(List)`: it could accidentally process a `List` of `String`s, especially when you start putting frameworks like Spring in between the caller and the callee, or when interfacing with non-generified clients, and the list goes on. So then, by this argument, why didn't we disallow generics in method arguments as well? – Mihai Danila Jan 19 '14 at 04:55
  • Just forgot catching of generic exceptions, that could by simply depended on raw types. It would be useful for returning values in exceptions. – Ondrej Bozek Jul 27 '15 at 12:20
  • 4
    @SuperJedi224 - No. It does them right - *given the constraint that generics **had to be** backwards compatible.* – Stephen C Feb 20 '17 at 07:25
  • @MihaiDanila that's what I thought too, but then I realized that *you cannot know what instances of your exception class would be thrown* so simply assuming "only `MyException`" will ever be thrown cannot be guaranteed. And if you could, what's the point of making it generic if you'll only ever instantiate it with `Integer` anyway? With method calls, the compiler can check calls to generic methods at compile time; it's a different story. – Frans Jan 05 '18 at 16:28
  • @Frans — who is "you" in "you cannot know what instances will be thrown"? If "you" means "the runtime", then I agree with you. The runtime doesn't know what type the element of List is or what type the payload of an Exception is. But if "you" is "the compiler", then you do know. More specifically, the compiler knows what you expect to *receive*. It could do the exact same thing with exception. Exact same rules. of course because of type erasure you can't catch both MyException and MyException, but why not be able to say you expect an Integer just like you do with `List`s. – Mihai Danila Jan 14 '18 at 03:49
  • @MihaiDanila As long as it's in the same unit of compilation, maybe. But as soon as you're linking with code that has already been compiled (and, hence, types have been erased) then you already don't know. And that goes both ways: the compiled code that would have said `throws MyException` now just says `throws MyException` and the compiled code that would have said `catch (MyException e)` now catches all `MyException`s. – Frans Jan 16 '18 at 11:52
  • @Franz: yes, I agree that you already don't know, but the same applies with `List`. How does List work with generics then? List is already in a different unit of compilation. Yes, `MyException` should be understood to "accept" all `MyException`s and should throw a cast exception if it doesn't contain strings (when accessed), just like `List` should be understood to "accept" all `List`s and should throw a class cast exception if it doesn't contain strings (when accessed). – Mihai Danila Jan 17 '18 at 14:01
  • @Franz: as soon as you are without source code, generics in Java become just syntactic sugar for writing less casting code. Then that could have be done with exceptions, not just with lists. Of course this whole conversation hinges on there being some Object contained within exceptions that needs casting. – Mihai Danila Jan 17 '18 at 14:09
  • Why didn't they just forbid to have two catch clauses with the same class after type-erasure then? – Arsen Dec 14 '21 at 14:15
23

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.

  • +1. my answer - http://stackoverflow.com/questions/30759692/throws-x-extends-exception-method-signature/30770099#30770099 – ZhongYu Jun 11 '15 at 02:14
  • 1
    The only way they could have designed it better was by rendering ~10 years of customers' code incompatible. That was a viable business decision. The design was correct ... **given the context**. – Stephen C Feb 20 '17 at 07:29
  • 1
    So how will you catch this exception? The only way that would work is to catch the raw type `EntityNotFoundException`. But that would render the generics useless. – Frans Jan 05 '18 at 16:24
17

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.

Apocalisp
  • 34,834
  • 8
  • 106
  • 155
IAdapter
  • 62,595
  • 73
  • 179
  • 242
6

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

outdev
  • 5,249
  • 3
  • 21
  • 38
2

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.

kdgregory
  • 38,754
  • 10
  • 77
  • 102
  • Yes, but why isn't it flagged as "unsafe" then, as with casts for example? – eljenso Feb 01 '09 at 19:17
  • Because with a cast, you are telling the compiler "I know that this execution path produces the expected result." With an exception, you can't say (for all possible exceptions) "I know where this was thrown." But, as I say above, it's a guess; I wasn't there. – kdgregory Feb 01 '09 at 19:29
  • "I know that this execution path produces the expected result." You don't know, you hope so. That's why generic and downcasts are statically unsafe, but they are nevertheless allowed. I upvoted Torsten's answer, because there I see the problem. Here I don't. – eljenso Feb 01 '09 at 20:04
  • If you don't know that an object is of a particular type, you shouldn't be casting it. The whole idea of a cast is that you have more knowledge than the compiler, and are making that knowledge explicitly part of the code. – kdgregory Feb 01 '09 at 22:02
  • Yes, and here you may have more knowledge than the compiler as well, since you want to do an unchecked conversion from MyException to MyException. Maybe you "know" it will be a MyException. – eljenso Feb 01 '09 at 22:31
  • But in the general case, you can't know that about exceptions. You might know it about your specific exception type, but not about any possible exception. So its disallowed. – kdgregory Feb 01 '09 at 22:34
  • I think a comparison could be made to the requirement that local variables (and parameters) used by an anonymous inner class must be final. The only reason for that requirement is that it provides a consistent mental model: the variable will not change independently in the class and method. – kdgregory Feb 01 '09 at 22:36
  • But again, I have no input into the JLS, so everything I've said in this answer and comments is speculation. – kdgregory Feb 01 '09 at 22:36
  • The reason local vars used by an inner class must be final and definitely assigned to, is because your inner class then can work on a copy of those variables to avoid scoping issues when the enclosing method exits. Not because of a mental model. Again I fail to see your point :) – eljenso Feb 02 '09 at 08:53
  • I have not written anything in the JLS either, but that does not mean we can't have a discussion about it. – eljenso Feb 02 '09 at 08:56
  • Local vars in anon classes are entirely about the mental model. As you note, they values are copied at construction. There's no physical reason not to let them change independently. The mental model, however, is that there's a single variable, so they shouldn't change independently. – kdgregory Feb 02 '09 at 12:10
  • Very nice and interesting discussion! :) – Hosam Aly Feb 02 '09 at 16:29
1

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

Alex Mandelias
  • 436
  • 5
  • 10