为啥 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 是正确的,它最终在运行时归结为 Objects。 (类文件当然包含关于 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 的未经检查的转换。也许你“知道”它会是一个 MyException.【参考方案3】:

正如马克所说,类型不可具体化,这在以下情况下是一个问题:

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

SomeException&lt;Integer&gt;SomeException&lt;String&gt; 都被擦除为同一类型,JVM 无法区分异常实例,因此无法判断应该执行哪个catch 块。

【讨论】:

但“可具体化”是什么意思? 所以规则不应该是“泛型类型不能继承 Throwable”,而是“catch 子句必须始终使用原始类型”。 他们可以禁止同时使用两个相同类型的 catch 块。所以单独使用 SomeExc 是合法的,只使用 SomeExc 和 SomeExc 是非法的。那不会有问题,或者会吗? 哦,现在我明白了。我的解决方案会导致 RuntimeExceptions 出现问题,而不必声明。因此,如果 SomeExc 是 RuntimeException 的子类,我可以抛出并显式捕获 SomeExc,但也许其他一些函数正在默默地抛出 SomeExc,而我的 SomeExc 的捕获块也会意外捕获。 @SuperJedi224 - 不。它做对了 - 考虑到泛型必须向后兼容的约束。【参考方案4】:

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

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

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&lt;Integer&gt; 将被转换为非泛型类型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】:

与问题无关,但如果你真的想要一个扩展Throwableinner 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 的泛型子类?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我要关心 Java 没有具体化的泛型?

不明白java中的泛型和抽象类有啥区别,感觉他们作用一样啊,为啥要用2种方法呢

泛型及java中的泛型

java的泛型

泛型的泛型的好处

为啥在接口列表的泛型类型中使用私有嵌套类型不是“不一致的可访问性”?