Java为什么不允许Throwable的通用子类?


146

根据Java语言分隔,第3版:

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

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

(据我所知,泛型只是编译时的语法糖,Object无论如何它们都将转换为.class文件,因此有效地声明泛型类就好像其中的所有内容都是Object。。如果我错了,请更正我)


1
通用类型参数由上限(默认情况下为Object)替换。如果您有类似List <?扩展A>,然后在类文件中使用A。
Torsten Marek

谢谢@Torsten。我以前没有想到这种情况。
Hosam Aly

2
这是一个很好的面试问题。
skaffman 2009年

@TorstenMarek:如果有人打电话myList.get(i),显然get仍会返回Object。编译器是否插入强制转换A以在运行时捕获某些约束?如果不是,则OP是正确的,最终它可以Object在运行时归结为。(该类文件中肯定包含有关的元数据A,但仅是AFAIK元数据。)
Mihai Danila 2014年

Answers:


155

正如标记所说,类型是不可更改的,在以下情况下会出现问题:

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

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


3
但是“可正当”是什么意思?
aberrant80

61
因此,规则不应为“泛型类型不能将Throwable子类化”,而是“ catch子句必须始终使用原始类型”。
Archie

3
他们可能只是不允许同时使用两个具有相同类型的捕获块。因此,单独使用SomeExc <Integer>是合法的,仅一起使用SomeExc <Integer>和SomeExc <String>是非法的。那不会有问题,或者会吗?
ViliamBúr13年

3
哦,现在我明白了。我的解决方案会导致RuntimeExceptions出现问题,而不必声明。因此,如果SomeExc是RuntimeException的子类,则可以抛出并显式捕获SomeExc <Integer>,但是其他函数可能会默默地抛出SomeExc <String>,而我的SomeExc <Integer>的catch块也会意外地捕获该异常。
ViliamBúr13年

4
@ SuperJedi224-否。考虑到泛型必须向后兼容的限制,
斯蒂芬C,

14

这是有关如何使用异常的简单示例:

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

因为异常不能是参数化的,所以语法受到限制,因此必须将类型写为标识符,并且不带任何后续参数。


2
您说“可验证”是什么意思?“可证明”不是一个词。
ForYourOwnGood

1
我不知道我自己的词,但在谷歌快速搜索了我:java.sun.com/docs/books/jls/third_edition/html/...
胡沙姆阿里


13

实质上是因为它的设计方式不好。

此问题阻止了干净的抽象设计,例如,

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

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

catch子句对泛型失败的事实没有得到证实,这不是借口。编译器可以简单地禁止扩展Throwable的具体泛型类型,也可以禁止catch子句中的泛型。



1
他们可以更好地设计它的唯一方法是渲染约10年的客户代码不兼容。那是一个可行的商业决定。设计是正确的... 根据上下文
斯蒂芬C,

1
那么您将如何捕获此异常?唯一可行的方法是捕获原始类型EntityNotFoundException。但这将使泛型无用。
弗朗斯

4

泛型在编译时检查类型正确性。然后,在称为类型擦除的过程中删除通用类型信息。例如,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");
}

由于类型为Erase,运行时将不知道要执行哪个catch块。

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

资料来源: 类型擦除问题


谢谢。这与Torsten提供的答案相同。
Hosam Aly

不,这不对。Torsten的答案对我没有帮助,因为它没有解释什么类型的擦除/验证是什么。
晚安书呆子骄傲

2

我希望这是因为没有办法保证参数化。考虑以下代码:

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

如您所述,参数化只是语法糖。但是,编译器试图确保参数化在编译范围内对对象的所有引用中保持一致。在发生异常的情况下,编译器无法保证仅从其正在处理的范围中引发MyException。


是的,但是为什么不像例如强制转换那样将其标记为“不安全”?
Eljenso,2009年

因为使用强制类型转换,您是在告诉编译器“我知道此执行路径会产生预期的结果”。除了一个例外,您不能(对于所有可能的例外)说:“我知道它被扔到哪里了。” 但是,正如我上面所说,这是一个猜测。我不在那里。
kdgregory,2009年

“我知道此执行路径会产生预期的结果。” 你不知道,你希望如此。这就是为什么常规和向下转换在静态上是不安全的,但是仍然允许它们。我赞成Torsten的回答,因为在那里我看到了问题。在这里我不知道。
eljenso

如果您不知道某个对象属于特定类型,则不应该强制转换它。强制转换的整个想法是,您比编译器拥有更多的知识,并将这些知识明确地成为代码的一部分。
kdgregory,2009年

是的,在这里您可能比编译器还具有更多的知识,因为您希望进行从MyException到MyException <Foo>的未经检查的转换。也许您“知道”它将是MyException <Foo>。
eljenso
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.