24
class Bouncy<T> extends Throwable {     
}
// Error: the generic class Bouncy<T> may not subclass java.lang.Throwable

Why doesn't Java support generic Throwables?

I realize that type erasure complicates certain things, but obviously Java gets by with a lot already, so why not push it one more notch and allow generic Throwables, with comprehensive compile-time check for potential problems?


I feel like the type erasure argument is rather weak. Currently, we can't do:

void process(List<String> list) {
}

void process(List<Integer> list) {
}

Of course, we get by without it. I'm not asking that we should be able to do catch Bouncy<T1> and Bouncy<T2> in the same try block, but if we use them in disjoint contexts with strict compile-time enforceable rules (which is pretty much the way generics works right now), wouldn't it be workable?

polygenelubricants
  • 376,812
  • 128
  • 561
  • 623
  • possible duplicate of [Why doesn't Java allow generic subclasses of Throwable?](http://stackoverflow.com/questions/501277/why-doesnt-java-allow-generic-subclasses-of-throwable) – C. Ross Aug 08 '12 at 17:47
  • Thanks for acknowledging the weakness in the "clash after type erasure" argument -- I feel it's being too easily accepted as the reason for the limitation so I took it upon myself to fight it. Of course, this also means that the accepted answer is also misleading and should probably be corrected. – Mihai Danila Jan 19 '14 at 04:15

5 Answers5

18

Java Language Specification 8.1.2 Generic Classes and Type Parameters:

This restriction is needed since the catch mechanism of the Java virtual machine works only with non-generic classes.

Personally, I think it's because we can't get any benefits of generics inside a catch clause. We can't write catch (Bouncy<String> ex) due to type erasure, but if we write catch (Bouncy ex), it would be useless to make it generic.

axtavt
  • 239,438
  • 41
  • 511
  • 482
  • 2
    Any idea why it happens that way? – zneak Mar 15 '10 at 02:04
  • 2
    Sorry, but I can't wrap my mind around the argument in the JLS. It's as if this statement forgets that the compiler performs type erasure, effectively removing the generics by the time the JVM uses its catch mechanism. So how is it necessary to avoid syntactic sugar if that syntactic sugar would have no impact on runtime anyway because it will be removed? And with this slippery slope, why not disallow other examples like the ones in my own anser below? I mean, the JVM doesn't support generics in those contexts either, yet they are allowed in source code. :) – Mihai Danila Jan 16 '14 at 19:06
9

Short answer: because they took shortcuts, just like they did with erasure.

Long answer: as others already indicated, because of erasure, there is no way to make a difference at runtime between a "catch MyException<String>" and "catch MyException<Integer>".

But that doesn't mean that there is no need for generic exceptions. I want generics to be able to use generic fields! They could have simply allowed generic exceptions, but only allow catching them in the raw state (e.g. "catch MyException").

Granted, this would make generics even more complicated. This is to show just how bad the decision to erase generics was. When will we have a Java version that supports real generics (with RTTI), not the current syntactic sugar?

  • One way to introduce real generics without breaking existing code is to introduce an annotation `@RealGeneric` that capture the type information into the annotation at compile time, and make it accessible at run time. – Earth Engine Jul 25 '13 at 00:46
  • The argument that erasure could lead to clashes if two catch blocks erased to the same type seems flawed to me -- see my answer. You could still allow generics in `catch` blocks and generate a compiler error if and when two catch blocks erased to the same type. The compiler does this today for method arguments: if you write `void process(List) {}`, the compiler doesn't complain -- it just erases it to `process(List)`. If you add a sibling that reads `void process(List) {}`, the compiler suddenly generates an error. (continued in next comment) – Mihai Danila Jan 19 '14 at 04:03
  • ... The same could have been done with `catch` blocks: allow `catch(MyException)` unless I also add a `catch(MyException)`. – Mihai Danila Jan 19 '14 at 04:06
  • 1
    And what if I _need_ to add a second catch? This would become way more confusing than the simple decision to allow no generic exception. – Vladimir Dyuzhev Jan 20 '14 at 04:49
  • And also, what if I have only one Foo class, and added a catch(Foo) statement, and then John created Foo, and that exception was thrown from the code... how would runtime know it shall NOT catch it in my catch? The type has been erased. That would become a version hell. For methods though that kind of conflict is known at compile time; for catch it is impossible to find out. – Vladimir Dyuzhev Jan 20 '14 at 04:52
  • @VladimirDyuzhev: you're making a good point there. But interestingly you could declare your method with `throws Foo`. If you think about it, when declaring a specific type parameter you promise to the compiler that you will use only the right kind of objects at runtime. You do this today with `List` and all. And even today, you could mismatch a `List` with a `List` in a variety of ways (oops, `ClassCastException`). Managing your generic types comes with the territory. It's just syntactic sugar for casting, right? And you can also skip the promises, by using `Foo>`. – Mihai Danila Jan 21 '14 at 02:13
8

Type erasure. Runtime exception type has no generics information. Thus you cannot do

} catch( Mistake<Account> ea) {
  ...
} catch( Mistake<User> eu) {
...
}

all you can do is

catch( Mistake ea ) {
  ...
}

And type erasure is how it was decided to preserve the backward compatibility when Java was moving from 1.4 to 1.5. Many people was unhappy then, and rightfully so. But having in mind the amount of deployed code, it was unthinkable to break code that worked happily in 1.4.

Vladimir Dyuzhev
  • 18,130
  • 10
  • 48
  • 62
  • See my answer for a rebuttal of this answer. Also, backward compatibility concerns did not prevent them from allowing generics in other contexts. You don't hear anyone complain that `List` can't be generified because we need to preserve backward compatibility. What you do hear is that generics get erased during the process called type erasure in order to preserve backward compatibility. The same could have been done for exceptions. – Mihai Danila Jan 16 '14 at 19:10
  • Please do not confuse information available to compiler (where type erasure is not performed yet) and to runtime (where it has been). Runtime catch statement has no way to tell one generic exception from another, while compiler has the generics information (though will erase it later). – Vladimir Dyuzhev Jan 18 '14 at 04:40
  • I'm saying the exact same thing in my answer. I'm also saying that this is not a reason to disallow generic exceptions. It's a reason to disallow `catch` blocks that erase to the same type, but that's about it. – Mihai Danila Jan 19 '14 at 03:29
  • Generic exception that cannot be caught in one of its specialization only has a little practical value. – Vladimir Dyuzhev Jan 20 '14 at 04:46
  • 1
    This is a much better argument than the statement "you can't catch Ex and Ex in the same context". But example: a framework that can manage objects from an entire domain model generically might throw a generic exception class `ValidationException`. A generic exception handler might want to catch `ValidationException>`, but more specific handlers might know the type and use `ValidationException`. The way things are today, I may have to cast explicitly (and lose the syntax sugar). Anyway, like I said, this sounds like a much better argument than the other ones. – Mihai Danila Jan 21 '14 at 02:03
5

You can still use generic methods, like this:

public class SomeException {
    private final Object target;

    public SomeException(Object target) {
        this.target = target;
    }

    public <T> T getTarget() {
        return (T) target;
    }
}

....

catch (SomeException e) {
    Integer target = e.getTarget();
}

I do agree with Cristian's answer above. While the accepted answer is technically correct (insofar as it references the JVM specs), Cristian Vasile's answer is one that qualifies and even challenges the limitation.

There are least two arguments that I noted in answers to this question that I do not agree with and that I will rebut. If the arguments in these answers were correct, we could use these arguments to attack generics in other contexts where they are used successfully today.


The first argument states that we can't use this:

catch (Exception<T1> e) {}

because the JVM doesn't know how to work with Exception<T1>. This argument would seem to also attack this use of generics, on the basis that the JVM doesn't know how to use List<T1>:

List<T1> list;

The argument, of course, forgets that the compiler performs type erasure and so the JVM doesn't need to know how to handle Exception<T1>. It can simply handle Exception, just like it handles List.

Of course, we could never handle catch(Exception<T1> e) and catch(Exception<T2> e) in the same try/catch because of type erasure, but then again, that's no worse than with method arguments or return values today: we don't handle myMethod(List<T1>) and myMethod(List<T2>) today either... (I reiterate this aspect in the second rebuttal below.)


A second argument goes as follows. We don't allow this:

catch (Exception<T1> e) {}

because this wouldn't work:

catch (Exception<T1> e) {}
catch (Exception<T2> e) {}

OK, then why not disallow this:

interface MyInterface {
    Comparable<Integer> getComparable();
}

because this does not work:

interface MyInterface {
    Comparable<Integer> getComparable();
    Comparable<String> getComparable();
}

or this:

interface MyInterface {
    void setComparable(Comparable<Integer> comparable);
}

because this does not work:

interface MyInterface {
    void setComparable(Comparable<Integer> comparable);
    void setComparable(Comparable<String> comparable);
}

In other words, why not disallow generics in most cases?

This second argument forgets that, although we couldn't possibly allow different generic constructs that that erase to the same non-generic construct in these contexts, we can still do the next best thing and allow generics as long as the types don't erase to the same type. That's what we do with method parameters: we allow you to use generics, but complain as soon as we detect duplicate signatures after type erasure. Well, we could have done about the same thing with exceptions and catch blocks...


In conclusion, I would expand on Cristian's answer. Instead of allowing generic exception classes and using the raw types in catch blocks:

class MyException<T> {}
...
catch (MyException e) { // raw

Java could have gone the whole way without problems:

class MyException<T> {}
...
catch (MyException<Foo> e) {
Mihai Danila
  • 2,229
  • 1
  • 23
  • 28
  • Some of my examples may appear confusing. When I give the two examples of method signatures that erase to the same signature, I refer to real examples if invalid situations that compliant compilers must refuse to compile. I merely point out that these situations (erasure to the same generic type) were not used by the language designers as a premise to ban generics across the board, but they seem to have been used to ban generics in throws clauses. Instead, the compiler was made to recognize and report such cases. It could have done the same for catch clauses. The choice seems arbitrary. – Mihai Danila Oct 26 '12 at 14:27
  • Statement "catch( Foo ex)" is executed about as "if ex instanceof Foo"; type erasure makes it impossible to tell it from "if ex instanceof Foo". List and List though work just fine with non-generic API at runtime; at compile time the type information is not erased yet, and so generics returns or setters are allowed. – Vladimir Dyuzhev Jan 18 '14 at 04:39
  • I believe your premise is flawed. Why would `catch (Foo ex)` need to be interpreted as `if (ex instanceof Foo)`? The compiler could perform type erasure, and the runtime would interpret the catch as `if (ex instanceof Foo)`. Runtime would work just like today, but we would have better compile time support thanks to generic exceptions. Of course, it would be impossible to catch both `Foo` and `Foo` in the same context, but that's OK: it's just as bad with method arguments today, since you can't have both `foo(List)` and `foo(List)` in the same context. – Mihai Danila Jan 19 '14 at 03:41
  • Yes, it would be impossible to tell Foo from Foo, and it is not OK. When I catch IOException, I want to catch only it, and not IOException. The workaround used today is to have specific exceptions (FileNotFound, ReadTimeout) where each of them can be caught separately if needed. – Vladimir Dyuzhev Jan 20 '14 at 04:42
  • You insist that support for generics inside exception classes would require runtime generics by showing us a very contrived example using standard exception classes. If Java supported generic exceptions -- and it's obvious to me that it *was* in fact theoretically possible to support these -- we would have used the generic type parameters for more mundane things. You have to start seeing generics for what they are: syntactic sugar with some support from the compiler for finding type mismatch errors. No bearing on the runtime. Nobody said that core exception classes would have to change either. – Mihai Danila Jan 21 '14 at 01:57
3

Here are a couple of things you can do:

  1. Throwables can implement generic interfaces, as long as the throwable itself has no type parameters, e.g.

    interface Bouncy<E> {
        // ...
    }
    class BouncyString extends Exception implements Bouncy<String> {
        // ...
    }

  2. A throws clause can refer to type parameters, e.g.
    static <X extends Throwable> void
    throwIfInstanceOf(Throwable ex, Class<X> clazz) throws X {
        if (clazz.isInstance(ex)) throw clazz.cast(ex);
    }
finnw
  • 47,861
  • 24
  • 143
  • 221
  • 1
    Heads up for alternatives. You can also have generic methods inside your exception class. As in @SuppressWarnings("unchecked") public T getObject() { return (T) object; }. – Mihai Danila Jan 19 '14 at 04:07