在Java中,定义通用异常类是非法的。编译器将拒绝编译以下内容:
public class Foo<T> extends Throwable {
// whatever...
}
但是,这个Scala代码编译得很好:
class Foo[T](val foo: T) extends Throwable
即使更奇怪,只要我抓住原始Foo
类型,我就可以在Java代码中使用此Scala类:
public class Main {
public static void main(String[] args) {
try {
throw new Foo<String>("test");
}
catch(Foo e) {
System.out.println(e.foo());
}
}
}
编译,运行并打印“test”。
这是根据JLS和JVM规范很好地定义的,还是偶然发生了?
Java对通用异常的限制纯粹是语言限制还是也适用于字节码(在这种情况下,Scala编译器生成的字节码无效)?
编辑:这是反编译后Scala类所看到的内容:
public class Foo<T> extends java.lang.Throwable {
public T value();
Code:
0: aload_0
1: getfield #15 // Field value:Ljava/lang/Object;
4: areturn
public Foo(T);
Code:
0: aload_0
1: aload_1
2: putfield #15 // Field value:Ljava/lang/Object;
5: aload_0
6: invokespecial #22 // Method java/lang/Throwable."<init>":()V
9: return
}
答案 0 :(得分:6)
简短回答:
答案很长:
Java语言规范说(第8.1.2节)关于声明这样的类:
如果泛型类是Throwable(第11.1.1节)的直接或间接子类,则是编译时错误。
由于Java虚拟机的catch机制仅适用于非泛型类,因此需要此限制。
它说抛出异常(§14.18):
throw语句中的表达式必须表示
1)引用类型的变量或值,可赋值(§5.2)到Throwable类型,或
2)出现空引用,或发生编译时错误。
Expression的引用类型将始终是一个类类型(因为没有可分配给Throwable的接口类型),它没有参数化(因为Throwable的子类不能是通用的(§8.1.2))。
当将泛型添加到Java语言时添加了此限制,因为它们未添加到JVM本身:JVM上只存在原始类型。
仍有关于该类型参数的信息,其中定义了类,但没有使用的位置!独立于参数化异常的声明,在JVM级别上这是不可能的:
try {
throw new Foo<String>("test");
} catch(Foo<Int> e) {
// int
} catch(Foo<String> e) {
// string
}
实现异常捕获的方法是有一个异常表,它指定要监视的字节码范围和要捕获的关联类。该类不能有类型参数,因为无法在字节码中描述它们(JVM规范,§2.10和§3.12)。
由于类型擦除,throw子句仅引用Foo
,并且这两个catch方法将成为异常表中的两个条目,它们都引用类Foo
,这两个条目都是无效的反正不可能。因此,在Java语言中无法使用语法,只能捕获Foo
。
因此,能够声明参数化异常变得非常无用且具有潜在危险性。因此,即使JVM本身并不关心,他们也完全被语言所禁止。
答案 1 :(得分:2)
以下是我们在讨论中分析的答案。
这是根据JLS和JVM规范很好地定义的,还是只是 碰巧意外工作?
根据Java语言规范,它不需要很好地定义,因为它是一种不同的语言。
JVM,OTOH,抛出和捕获此类异常没有问题,因为由于类型擦除,它在字节码中没有区别。
然而,有趣的问题仍然存在,为什么Java首先禁止使用泛型类扩展Throwable。