根据Java 语言规范,第 3 版:
我想了解为什么做出这个决定。通用异常有什么问题?
(据我所知,泛型只是编译时的语法糖,Object
无论如何它们都会在.class
文件中被翻译,所以有效地声明一个泛型类就好像它里面的所有东西都是一个Object
. 如果我错了,请纠正我.)
根据Java 语言规范,第 3 版:
我想了解为什么做出这个决定。通用异常有什么问题?
(据我所知,泛型只是编译时的语法糖,Object
无论如何它们都会在.class
文件中被翻译,所以有效地声明一个泛型类就好像它里面的所有东西都是一个Object
. 如果我错了,请纠正我.)
正如马克所说,这些类型是不可具体化的,这在以下情况下是一个问题:
try {
doSomeStuff();
} catch (SomeException<Integer> e) {
// ignore that
} catch (SomeException<String> e) {
crashAndBurn()
}
两者SomeException<Integer>
和SomeException<String>
都被擦除为相同的类型,JVM 无法区分异常实例,因此无法判断catch
应该执行哪个块。
这是一个如何使用异常的简单示例:
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
因为异常不能是参数的,所以语法受到限制,因此类型必须写为标识符,没有后面的参数。
本质上是因为它的设计方式很糟糕。
这个问题阻止了干净的抽象设计,例如,
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 的直接或间接子类,则会出现编译时错误。
来源: 类型擦除问题
我希望这是因为无法保证参数化。考虑以下代码:
try
{
doSomethingThatCanThrow();
}
catch (MyException<Foo> e)
{
// handle it
}
正如您所注意到的,参数化只是语法糖。但是,编译器会尝试确保参数化在编译范围内对对象的所有引用中保持一致。在发生异常的情况下,编译器无法保证 MyException 仅从它正在处理的范围内抛出。
与问题不太相关,但如果你真的想要一个inner class
扩展 aThrowable
你可以声明它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