为什么 Java 不允许 Throwable 的泛型子类?

根据 Java 语言规范第三版:

如果泛型类是 Throwable的直接或间接子类,则为编译时错误。

我希望理解为什么做出这个决定。通用异常有什么问题吗?

(据我所知,泛型只是编译时的语法糖,它们无论如何都会在 .class文件中被翻译成 Object,因此有效地声明一个泛型类就好像它里面的所有东西都是 Object一样。如果我说错了,请纠正我。)

38216 次浏览

下面是如何使用异常的一个简单示例:

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

TRy 语句体引发具有给定值的异常,该值由 catch 子句捕获。

相反,禁止下面的新异常定义,因为它创建了一个参数化类型:

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

尝试编译上述报告时出错:

% 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

这种限制是合理的,因为几乎任何捕获此类异常的尝试都必须失败,因为该类型是不可重新调整的。例外的典型用法可能如下所示:

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;
}
}
}

这是不允许的,因为 catch 子句中的类型是不可再现的。在撰写本文时,Sun 编译器报告了一系列语法错误,例如:

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

由于异常不能是参数,因此语法受到限制,因此类型必须 被写成一个标识符,没有下面的参数。

我认为这是因为没有办法保证参量化:

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

正如你注意到的,参量化只是句法上的糖。不过,编译器会尝试确保编译范围内所有对象引用的参量化保持一致。在异常的情况下,编译器无法保证 MyException 只从它正在处理的作用域中引发。

正如马克所说,这些类型是不可再现的,这在以下情况下是一个问题:

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

SomeException<Integer>SomeException<String>都被擦除为相同的类型,JVM 无法区分异常实例,因此无法判断应该执行哪个 catch块。

本质上是因为它的设计方式很糟糕。

这个问题阻碍了干净的抽象设计,例如,

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


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

一个 catch 子句会因泛型而失败而没有被具体化,这个事实并不能成为失败的借口。 编译器可以简单地禁用扩展 Throwable 的具体泛型类型,或者禁用 catch 子句内的泛型。

泛型在编译时检查类型是否正确。然后在称为 类型删除的进程中删除泛型类型信息。例如,List<Integer>将被转换为非泛型类型 List

由于 类型删除的原因,无法在运行时确定类型参数。

让我们假设允许像下面这样扩展 Throwable:

public class GenericException<T> extends Throwable

现在让我们考虑以下代码:

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

由于 类型删除的原因,运行时将不知道要执行哪个 catch 块。

因此,如果泛型类是 Throwable 的直接或间接子类,那么就是编译时错误。

资料来源: < a href = “ https://en.wikipedia.org/wiki/Generics _ in _ Java # 問題 _ with _ type _ erasure”rel = “ noReferrer”> type 擦除的问题

不是 也是相关的问题,但如果你真的想有一个 inner class,扩展一个 Throwable,你可以声明它 static。如果 Throwable在逻辑上与封闭类相关,但不与该封闭类的特定泛型类型相关,则可以这样做。通过声明它为 static,它就不会绑定到封闭类的实例,因此问题就消失了。

下面的例子(诚然不是很好)说明了这一点:

/** 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));
}
}
}

延伸阅读: Docs.oracle.com