【问题标题】:Why doesn't Java support generic Throwables?为什么 Java 不支持泛型 Throwables?
【发布时间】:2011-01-27 12:40:36
【问题描述】:
class Bouncy<T> extends Throwable {     
}
// Error: the generic class Bouncy<T> may not subclass java.lang.Throwable

为什么 Java 不支持泛型 Throwables?

我意识到类型擦除会使某些事情复杂化,但显然 Java 已经完成了很多工作,那么为什么不将其推高一个档次并允许泛型 Throwables,并通过全面的编译时检查潜在问题?


我觉得类型擦除参数相当弱。目前,我们不能这样做:

void process(List<String> list) {
}

void process(List<Integer> list) {
}

当然,没有它我们也能过得去。我不是要求我们应该能够在同一个 try 块中执行 catch Bouncy&lt;T1&gt; 和 Bouncy&lt;T2&gt;,但是如果我们在具有严格编译时可执行规则的不相交上下文中使用它们(这几乎是泛型的方式现在可以使用),它不可行吗?

【问题讨论】:

  • 感谢您承认“类型擦除后的冲突”论点中的弱点——我觉得它太容易被接受为限制的原因,所以我自己来对抗它。当然,这也意味着接受的答案也具有误导性,应该予以纠正。

标签: java generics oop throwable


【解决方案1】:

Java 语言规范 8.1.2 Generic Classes and Type Parameters:

这个限制是必要的,因为 Java 虚拟机的 catch 机制只适用于非泛型类。

就个人而言,我认为这是因为我们无法在catch 子句中获得泛型的任何好处。由于类型擦除,我们不能写catch (Bouncy&lt;String&gt; ex),但是如果我们写catch (Bouncy ex),让它泛型就没用了。

【讨论】:

  • 知道为什么会这样吗?
  • 抱歉,我无法完全理解 JLS 中的论点。就好像这个语句忘记了编译器执行类型擦除,在 JVM 使用它的 catch 机制时有效地删除了泛型。那么,如果语法糖无论如何都会对运行时没有影响,因为它将被删除,那么如何避免语法糖呢?有了这个滑坡,为什么不禁止其他示例,例如下面我自己的分析器中的示例?我的意思是,JVM 在这些上下文中也不支持泛型,但它们在源代码中是允许的。 :)
【解决方案2】:

类型擦除。运行时异常类型没有泛型信息。所以你做不到

} catch( Mistake<Account> ea) {
  ...
} catch( Mistake<User> eu) {
...
}

你能做的就是

catch( Mistake ea ) {
  ...
}

类型擦除是在 Java 从 1.4 迁移到 1.5 时决定保留向后兼容性的方式。许多人当时不高兴,这是理所当然的。但是考虑到部署代码的数量,破坏在 1.4 中运行良好的代码是不可想象的。

【讨论】:

  • 查看我的答案以反驳这个答案。此外,向后兼容性问题并没有阻止他们在其他上下文中允许泛型。您不会听到有人抱怨 List 无法泛化,因为我们需要保持向后兼容性。您所听到的是,泛型在称为类型擦除的过程中被擦除,以保持向后兼容性。对于异常也可以这样做。
  • 请不要混淆编译器可用的信息(尚未执行类型擦除)和运行时(已经存在)。运行时 catch 语句无法区分一个泛型异常与另一个泛型异常,而编译器拥有泛型信息(尽管稍后会删除它)。
  • 我在回答中说了同样的话。我还说这不是禁止通用异常的理由。这是禁止 catch 擦除为相同类型的块的原因,但仅此而已。
  • 无法在其特化中捕获的泛型异常只有一点实用价值。
  • 这是一个比“你不能在同一个上下文中捕获 Ex 和 Ex”的陈述更好的论据。但是例如:一个可以管理来自整个域模型的对象的框架通常可能会抛出一个通用异常类ValidationException&lt;T&gt;。通用异常处理程序可能想要捕获ValidationException&lt;?&gt;,但更具体的处理程序可能知道类型并使用ValidationException&lt;UserPreferences&gt;。今天的情况,我可能不得不显式地强制转换(并且失去语法糖)。无论如何,就像我说的,这听起来比其他的更好。
【解决方案3】:

以下是您可以做的几件事:

  1. Throwables 可以实现泛型接口,只要 throwable 本身没有类型参数,例如

    interface Bouncy&lt;E&gt; {
        // ...
    }
    class BouncyString extends Exception implements Bouncy&lt;String&gt; {
        // ...
    }

  2. throws 子句可以引用类型参数,例如
    static &lt;X extends Throwable&gt; void
    throwIfInstanceOf(Throwable ex, Class&lt;X&gt; clazz) throws X {
        if (clazz.isInstance(ex)) throw clazz.cast(ex);
    }

【讨论】:

  • 寻找替代方案。您还可以在异常类中使用通用方法。如@SuppressWarnings("unchecked") public T getObject() { return (T) object; }.
【解决方案4】:

简短的回答:因为他们走捷径,就像他们使用擦除一样。

长答案:正如其他人已经指出的那样,由于擦除,在运行时无法区分“catch MyException”和“catch MyException”。

但是这并不意味着不需要通用例外。我希望泛型能够使用泛型字段!他们本可以简单地允许通用异常,但只允许在原始状态下捕获它们(例如“catch MyException”)。

当然,这会使泛型变得更加复杂。 这是为了表明删除仿制药的决定是多么糟糕。我们什么时候会有支持真正泛型(使用 RTTI)而不是当前语法糖的 Java 版本?

【讨论】:

  • 在不破坏现有代码的情况下引入真正的泛型的一种方法是引入注解@RealGeneric,在编译时将类型信息捕获到注解中,并使其在运行时可访问。
  • 如果两个 catch 块被擦除为同一类型,擦除可能导致冲突的论点对我来说似乎是有缺陷的——请参阅我的答案。您仍然可以在catch 块中允许泛型,并在两个catch 块擦除为相同类型时生成编译器错误。编译器今天为方法参数执行此操作:如果您编写void process(List&lt;String&gt;) {},编译器不会抱怨——它只是将其擦除为process(List)。如果添加读取void process(List&lt;Object&gt;) {} 的兄弟,编译器会突然产生错误。 (在下一条评论中继续)
  • ... catch 块也可以这样做:允许 catch(MyException&lt;String&gt;) 除非我还添加了 catch(MyException&lt;Integer&gt;)。
  • 如果我需要添加第二个catch怎么办?这将比不允许​​一般例外的简单决定更令人困惑。
  • 另外,如果我只有一个 Foo 类,并添加了一条 catch(Foo) 语句,然后 John 创建了 Foo,并且该异常是从代码......运行时如何知道它不会在我的捕获中捕获它?该类型已被删除。那将成为版本地狱。对于方法,尽管这种冲突在编译时是已知的;对于 catch 是不可能找到的。
【解决方案5】:

您仍然可以使用泛型方法,如下所示:

public class SomeException {
    private final Object target;

    public SomeException(Object target) {
        this.target = target;
    }

    public <T> T getTarget() {
        return (T) target;
    }
}

....

catch (SomeException e) {
    Integer target = e.getTarget();
}

我同意克里斯蒂安的上述回答。虽然公认的答案在技术上是正确的(就其引用 JVM 规范而言),但 Cristian Vasile 的答案是符合甚至挑战限制的答案。

我在回答这个问题时指出了至少两个我不同意并且我将反驳的论点。如果这些答案中的论点是正确的,我们可以使用这些论点在今天成功使用它们的其他上下文中攻击泛型。


第一个参数声明我们不能使用这个:

catch (Exception<T1> e) {}

因为 JVM 不知道如何使用 Exception&lt;T1&gt;。这个论点似乎也在攻击泛型的这种使用,因为 JVM 不知道如何使用List&lt;T1&gt;:

List<T1> list;

当然,该参数忘记了编译器执行类型擦除,因此 JVM 不需要知道如何处理 Exception&lt;T1&gt;。它可以简单地处理Exception,就像它处理List一样。

当然,由于类型擦除,我们永远无法在同一个 try/catch 中处理 catch(Exception&lt;T1&gt; e) 和 catch(Exception&lt;T2&gt; e),但话又说回来,这并不比今天使用方法参数或返回值更糟糕:我们不处理 @ 987654331@ 和 myMethod(List&lt;T2&gt;) 今天也可以……(我在下面的第二次反驳中重申了这一点。)


第二个参数如下。我们不允许这样做:

catch (Exception<T1> e) {}

因为这行不通:

catch (Exception<T1> e) {}
catch (Exception<T2> e) {}

好的,那为什么不禁止这个:

interface MyInterface {
    Comparable<Integer> getComparable();
}

因为这不起作用:

interface MyInterface {
    Comparable<Integer> getComparable();
    Comparable<String> getComparable();
}

或者这个:

interface MyInterface {
    void setComparable(Comparable<Integer> comparable);
}

因为这不起作用:

interface MyInterface {
    void setComparable(Comparable<Integer> comparable);
    void setComparable(Comparable<String> comparable);
}

换句话说,为什么在大多数情况下不禁止泛型?

第二个论点忘记了,尽管我们不可能允许在这些上下文中擦除到相同非泛型构造的不同泛型构造,但我们仍然可以做次优的事情,并且允许泛型只要类型不会擦除为相同的类型。这就是我们对方法参数所做的事情:我们允许您使用泛型,但一旦我们在类型擦除后检测到重复签名就会抱怨。好吧,我们可以用异常和 catch 块做同样的事情......


最后,我将扩展 Cristian 的答案。而不是允许通用异常类和使用catch块中的原始类型:

class MyException<T> {}
...
catch (MyException e) { // raw

Java 完全可以毫无问题地完成:

class MyException<T> {}
...
catch (MyException<Foo> e) {

【讨论】:

  • 我的一些示例可能看起来令人困惑。当我给出擦除相同签名的方法签名的两个示例时,如果兼容编译器必须拒绝编译的无效情况,我会参考实际示例。我只是指出,语言设计者没有将这些情况(擦除到相同的泛型类型)用作全面禁止泛型的前提,但它们似乎已被用于在 throws 子句中禁止泛型。相反,编译器被用来识别和报告这种情况。它可以对 catch 子句做同样的事情。选择似乎是任意的。
  • 语句 "catch( Foo ex)" 被执行为 "if ex instanceof Foo";类型擦除使得无法从“if ex instanceof Foo”中分辨出来。 List 和 List 虽然在运行时与非通用 API 一起工作得很好;在编译时,类型信息还没有被删除,因此泛型返回或设置器是允许的。
  • 我相信你的前提是有缺陷的。为什么catch (Foo&lt;Bar&gt; ex) 需要解释为if (ex instanceof Foo&lt;Bar&gt;)?编译器可以执行类型擦除,运行时会将捕获解释为if (ex instanceof Foo)。运行时将像今天一样工作,但由于通用异常,我们将获得更好的编译时支持。当然,不可能在同一上下文中同时捕获 Foo&lt;Bar&gt; 和 Foo&lt;Baz&gt;,但这没关系:今天的方法参数同样糟糕,因为您不能同时拥有 foo(List&lt;String&gt;) 和 foo(List&lt;Object&gt;)相同的上下文。
  • 是的,不可能将 Foo 与 Foo 区分开来,而且也不行。当我捕获 IOException 时,我只想捕获它,而不是 IOException。今天使用的解决方法是设置特定的异常(FileNotFound、ReadTimeout),如果需要,可以单独捕获它们中的每一个。
  • 您坚持认为异常类中对泛型的支持需要运行时泛型,向我们展示了一个使用标准异常类的非常人为的示例。如果 Java 支持泛型异常——而且对我来说很明显它是事实上理论上可能支持这些——我们会使用泛型类型参数来处理更普通的事情。您必须开始了解泛型的本质:在编译器的某些支持下查找类型不匹配错误的语法糖。与运行时无关。没有人说核心异常类也必须改变。
猜你喜欢
  • 2021-07-06
  • 1970-01-01
  • 1970-01-01
  • 2012-11-15
  • 2015-05-23
  • 1970-01-01
  • 2010-09-07
相关资源
最近更新 更多