Why doesn't Java allow generic subclasses of Throwable?
Asked Answered
W

6

186

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.)

Willem answered 1/2, 2009 at 18:7 Comment(4)
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.Cleanshaven
Thank you @Torsten. I didn't think of that case before.Willem
It's a good interview question, this one.Outrider
@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 Objects at runtime. (The class file certainly contains metadata about A, but it's only metadata AFAIK.)Juetta
C
184

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.

Cleanshaven answered 1/2, 2009 at 18:22 Comment(14)
but what does "reifiable" mean?Cupronickel
@aberrant80: simply speaking it means that you can get to the concret type at runtime. Which is not the fact in Java.Ashia
So the rule should not be "generic types cannot subclass Throwable" but instead "catch clauses must always use raw types".Quillen
They could just disallow using two catch blocks with the same type together. So that using SomeExc<Integer> alone would be legal, only using SomeExc<Integer> and SomeExc<String> together would be illegal. That would make no problems, or would it?Culley
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<Integer>, but maybe some other function is silently throwing SomeExc<String> and my catch block for SomeExc<Integer> would accidentally catch that too.Culley
@ViliamBúr: nonetheless, the same risk is present today with things like process(List<Integer>): it could accidentally process a List of Strings, 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?Juetta
Just forgot catching of generic exceptions, that could by simply depended on raw types. It would be useful for returning values in exceptions.Darken
@SuperJedi224 - No. It does them right - given the constraint that generics had to be backwards compatible.Minify
@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<Integer>" 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.Perspective
@Perspective — 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<Integer> and MyException<String>, but why not be able to say you expect an Integer just like you do with Lists.Juetta
@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<String> now just says throws MyException and the compiled code that would have said catch (MyException<String> e) now catches all MyExceptions.Perspective
@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<String> should be understood to "accept" all MyExceptions and should throw a cast exception if it doesn't contain strings (when accessed), just like List<String> should be understood to "accept" all Lists and should throw a class cast exception if it doesn't contain strings (when accessed).Juetta
@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.Juetta
Why didn't they just forbid to have two catch clauses with the same class after type-erasure then?Folkestone
C
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.

Crossbred answered 9/2, 2015 at 11:21 Comment(3)
+1. my answer - #30760192Foreland
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.Minify
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.Perspective
G
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.

Genuflection answered 1/2, 2009 at 18:11 Comment(3)
What do you mean when you say 'reifiable'? 'reifiable' is not a word.Parthinia
I didn't know the word myself, but a quick search in google got me this: java.sun.com/docs/books/jls/third_edition/html/…Willem
docs.oracle.com/javase/tutorial/java/generics/…Panettone
T
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

Tigress answered 30/9, 2017 at 11:35 Comment(2)
Thanks. This is the same answer as the one provided by Torsten.Willem
No it's not. Torsten's answer didn't help me, because it didn't explain what type erasure/reification is.Selfabnegation
N
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.

Naara answered 1/2, 2009 at 18:12 Comment(12)
Yes, but why isn't it flagged as "unsafe" then, as with casts for example?Audrey
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.Naara
"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.Audrey
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.Naara
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<Foo>. Maybe you "know" it will be a MyException<Foo>.Audrey
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.Naara
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.Naara
But again, I have no input into the JLS, so everything I've said in this answer and comments is speculation.Naara
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 :)Audrey
I have not written anything in the JLS either, but that does not mean we can't have a discussion about it.Audrey
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.Naara
Very nice and interesting discussion! :)Willem
I
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

Imaginative answered 18/5, 2021 at 6:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.