为啥 Java 不允许 Throwable 的泛型子类?
Posted
技术标签:
【中文标题】为啥 Java 不允许 Throwable 的泛型子类?【英文标题】:Why doesn't Java allow generic subclasses of Throwable?为什么 Java 不允许 Throwable 的泛型子类? 【发布时间】:2010-10-04 19:35:21 【问题描述】:根据Java Language Sepecification,第3版:
It is a compile-time error if a generic class is a direct or indirect subclass of
Throwable
.
我想了解做出此决定的原因。泛型异常有什么问题?
(据我所知,泛型只是编译时语法糖,无论如何它们都会在.class
文件中转换为Object
,因此有效地声明一个泛型类就好像其中的所有内容都是一个Object
。如果我错了,请纠正我。)
【问题讨论】:
泛型类型参数被上限替换,默认情况下为 Object。如果你有类似 List,然后在类文件中使用 A。 谢谢@Torsten。我以前没想过这种情况。 这是一道很好的面试题。 @TorstenMarek:如果有人调用myList.get(i)
,显然get
仍会返回Object
。编译器是否将强制转换插入A
以便在运行时捕获一些约束?如果不是,那么 OP 是正确的,它最终在运行时归结为 Object
s。 (类文件当然包含关于 A
的元数据,但它只是 AFAIK 的元数据。)
【参考方案1】:
这是一个如何使用异常的简单示例:
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
由于异常不能是参数的,因此语法受到限制,因此类型必须 写成标识符,不带后面的参数。
【讨论】:
你说的“可具体化”是什么意思? 'reifiable' 不是一个词。 我自己不知道这个词,但在谷歌快速搜索得到了这个:java.sun.com/docs/books/jls/third_edition/html/… docs.oracle.com/javase/tutorial/java/generics/…【参考方案2】:我希望这是因为无法保证参数化。考虑以下代码:
try
doSomethingThatCanThrow();
catch (MyException<Foo> e)
// handle it
正如您所注意到的,参数化只是语法糖。但是,编译器会尝试确保参数化在编译范围内对对象的所有引用中保持一致。在发生异常的情况下,编译器无法保证 MyException 仅从它正在处理的范围内抛出。
【讨论】:
是的,但为什么不将其标记为“不安全”,例如强制转换? 因为有了演员表,你告诉编译器“我知道这个执行路径会产生预期的结果。”除了一个例外,你不能说(对于所有可能的例外)“我知道这是在哪里抛出的。”但是,正如我上面所说,这是一个猜测;我不在场。 "我知道这个执行路径会产生预期的结果。"你不知道,你希望如此。这就是为什么泛型和向下转型在静态上是不安全的,但它们仍然是允许的。我赞成 Torsten 的回答,因为我看到了问题所在。这里我没有。 如果您不知道某个对象属于特定类型,则不应强制转换它。强制转换的整个想法是,您比编译器拥有更多的知识,并且将这些知识明确地作为代码的一部分。 是的,在这里你可能比编译器有更多的知识,因为你想做一个从 MyException 到 MyException正如马克所说,类型不可具体化,这在以下情况下是一个问题:
try
doSomeStuff();
catch (SomeException<Integer> e)
// ignore that
catch (SomeException<String> e)
crashAndBurn()
SomeException<Integer>
和SomeException<String>
都被擦除为同一类型,JVM 无法区分异常实例,因此无法判断应该执行哪个catch
块。
【讨论】:
但“可具体化”是什么意思? 所以规则不应该是“泛型类型不能继承 Throwable”,而是“catch 子句必须始终使用原始类型”。 他们可以禁止同时使用两个相同类型的 catch 块。所以单独使用 SomeExc本质上是因为它的设计方式很糟糕。
这个问题阻止了干净的抽象设计,例如,
public interface Repository<ID, E extends Entity<ID>>
E getById(ID id) throws EntityNotFoundException<E, ID>;
catch 子句会因为泛型没有具体化而失败的事实不能作为借口。 编译器可以简单地禁止扩展 Throwable 的具体泛型类型或禁止在 catch 子句中使用泛型。
【讨论】:
+1。我的回答 - ***.com/questions/30759692/… 他们可以更好地设计它的唯一方法是使大约 10 年的客户代码不兼容。这是一个可行的商业决策。设计是正确的...鉴于上下文。 那么你将如何捕捉这个异常呢?唯一可行的方法是捕获原始类型EntityNotFoundException
。但这会使泛型变得无用。【参考方案5】:
在编译时检查泛型的类型正确性。然后在称为类型擦除的过程中删除通用类型信息。例如,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 的直接或间接子类,则会出现编译时错误。
来源:Problems with type erasure
【讨论】:
谢谢。这与the one provided by Torsten 的答案相同。 不,不是。 Torsten 的回答对我没有帮助,因为它没有解释擦除/具体化是什么类型。【参考方案6】:太与问题无关,但如果你真的想要一个扩展Throwable
的inner class
,你可以声明它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
【讨论】:
以上是关于为啥 Java 不允许 Throwable 的泛型子类?的主要内容,如果未能解决你的问题,请参考以下文章