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

185

2
泛型类型参数被替换为上限,其默认值为Object。如果你有像List<? extends A>这样的东西,那么A将在类文件中使用。 - Torsten Marek
谢谢@Torsten。我之前没有考虑到那种情况。 - Hosam Aly
2
这是一个不错的面试问题。 - skaffman
@TorstenMarek:如果调用myList.get(i),显然get仍然返回一个Object。编译器是否插入了一个转换为A的强制类型转换以在运行时捕获一些约束条件?如果没有,那么OP是正确的,在最后它归结为运行时的Object。(类文件肯定包含有关A的元数据,但据我所知,这只是元数据。) - Mihai Danila
6个回答

183

正如Mark所说,这些类型是不可具体化的,在以下情况下会产生问题:

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

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


8
“reifiable” 是什么意思? - aberrant80
85
规则不应该是“泛型类型不能继承Throwable”,而是“捕获异常时必须始终使用原始类型”。 - Archie
3
他们可以禁止同时使用相同类型的两个catch块,这样仅使用SomeExc<Integer>是合法的,而仅使用SomeExc<Integer>和SomeExc<String>是不合法的。这样做不会出现问题,或者会出现问题吗? - Viliam Búr
4
哦,现在我明白了。我的解决方案会导致RuntimeException问题,因为它们不需要声明。所以,如果SomeExc是RuntimeException的子类,我可以抛出并明确捕获SomeExc<Integer>,但是可能其他一些函数悄悄地抛出SomeExc<String>,那么我对SomeExc<Integer>的catch块也会意外地捕获这个异常。 - Viliam Búr
4
不。考虑到泛型必须保持向后兼容性,它们做得很好。 - Stephen C
显示剩余9条评论

23

这主要是因为它的设计存在问题。

这个问题会阻碍简洁的抽象设计,例如:

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

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

泛型类型无法具体化可能导致catch子句失败,但这并不是借口。编译器可以简单地禁止扩展Throwable的具体通用类型或禁止在catch子句中使用泛型。


+1. 我的回答 - https://dev59.com/vl0a5IYBdhLWcg3wD1Ae#30770099 - ZhongYu
2
他们唯一能够更好地设计它的方式是使大约10年的客户代码不兼容。这是一个可行的商业决策。鉴于上下文,该设计是正确的... - Stephen C
1
那么你将如何捕获这个异常?唯一可行的方式是捕获原始类型EntityNotFoundException。但这将使泛型失去意义。 - Frans

17

下面是一个使用异常处理的简单示例:

class IntegerExceptionTest {
  public static void main(String[] args) {
    try {
      throw new IntegerException(42);
    } catch (IntegerException e) {
      assert e.getValue() == 42;
    }
  }
}
语句的主体使用给定值抛出异常,该异常被子句捕获。 相反,下面的定义是被禁止的,因为它创建了一个带参数的类型的新异常:

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
当你说“reifiable”时,你的意思是什么?“reifiable”不是一个单词。 - ForYourOwnGood
2
我自己不知道这个词,但是在谷歌上快速搜索到了这个链接:http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.7 - Hosam Aly
https://docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html - Stijn de Witt

6

泛型在编译时进行类型正确性检查。然后通过一种叫做“类型擦除”的过程来删除泛型类型信息。例如,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");
}

由于类型擦除,运行时将不知道执行哪个捕获块。

因此,如果通用类是Throwable的直接或间接子类,则会出现编译时错误。

来源:类型擦除问题


谢谢。这个答案与Torsten提供的答案相同。 - Hosam Aly
2
不是的。Torsten的答案没有帮助我,因为它没有解释类型擦除/再ification是什么。 - Good Night Nerd Pride

2
我认为这是因为无法保证参数化。考虑以下代码:
try
{
    doSomethingThatCanThrow();
}
catch (MyException<Foo> e)
{
    // handle it
}

正如您所指出的,参数化只是一种语法糖。然而,编译器试图确保在编译范围内所有引用对象的参数化保持一致。在异常情况下,编译器无法保证MyException仅从它正在处理的范围中抛出。


因为通过一个转型,你在告诉编译器“我知道这个执行路径会产生期望的结果”。而对于异常,你不能说(针对所有可能的异常)“我知道它抛出的位置”。但是,正如我上面所说,这只是一种猜测;我并不确定。 - kdgregory
我知道这个执行路径会产生预期的结果。你不知道,你只是希望如此。这就是为什么泛型和向下转型在静态上是不安全的,但仍然被允许存在。我对Torsten的回答投了赞成票,因为那里我看到了问题所在。在这里,我看不到问题。 - eljenso
如果你不知道一个对象是什么类型,就不应该对它进行强制转换。强制转换的整个概念是你拥有比编译器更多的知识,并且正在将该知识明确地纳入代码中。 - kdgregory
我认为可以将其比作匿名内部类使用的局部变量(和参数)必须是final的要求。该要求的唯一原因是它提供了一个一致的心理模型:变量不会在类和方法中独立更改。 - kdgregory
匿名类中的本地变量完全是关于心理模型的。就像您所提到的,它们的值在构造时被复制。没有任何物理原因不让它们独立更改。然而,心理模型是存在一个单一变量,因此它们不应该独立更改。 - kdgregory
显示剩余7条评论

1

虽然与问题不太相关,但如果您真的想要一个扩展了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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接