我想编写一个未选中的自定义可抛出对象。
有一些方法可以在抛出时欺骗编译器(例如,将抛出的可抛物重新抛出为未选中的实用程序类?),我已经实现了:
public class CustomThrowable extends Throwable {
public CustomThrowable(String message) {
super(message);
}
@SuppressWarnings("unchecked")
public <T extends Throwable> T unchecked() throws T {
throw (T) this;
}
}
但我在"赶上时间"遇到了问题:
try {
throw new CustomThrowable("foo").unchecked();
}
catch (CustomThrowable t) { } // <- compiler error because the try block does not "throw" a CustomThrowable
有没有办法简单地实现未经检查的Throwable
,还是RuntimeException
唯一的方法?我曾想避免从RuntimeException
继承,因为我的可抛掷物不是例外,而是一个收益指令。
更新: 避免扩展RuntimeException
的另一个原因是我的CustomThrowable
会被通用catch (Exception ex) { }
块捕获。因此,如果我想从堆栈内传达信息,每一层都需要意识到CustomThrowable
可能会通过并显式捕获-重新抛出它;这种意识是人们在使用Throwable
设计时试图避免的很大一部分。
您可以扩展Error
而不是Throwable
。JavaError
类是一个未经检查的Throwable
。
Throwable
是一个已检查的错误,实际上应该不可能抛出这样的错误,除非它是运行时异常或错误。
但实际上这是可能的。下面是一个示例。
这是一个实用程序,它抛出任何未选中的异常:
package org.mentallurg;
public class ExceptionUtil {
private static class ThrowableWrapper extends Throwable {
private Throwable throwable;
public ThrowableWrapper(Throwable throwable) {
super();
this.throwable = throwable;
}
@SuppressWarnings("unchecked")
public <T extends Throwable> T throwNested() throws T {
throw (T) throwable;
}
}
private static <T extends Throwable> T throwThis(T throwable) throws T {
throw throwable;
}
public static <T extends Throwable> void throwUnchecked(T throwable) {
new ThrowableWrapper(throwable).throwNested();
}
}
下面是一个用法示例:
package org.mentallurg;
public class Test {
private static void doSomething() {
Exception checkedException = new Exception("I am checked exception");
ExceptionUtil.throwUnchecked(checkedException);
}
public static void main(String[] args) {
doSomething();
}
}
请注意,没有throws Throwable
条款,无论是main
还是doSomething
。并且没有编译错误。在RuntimeException
的情况下,这是可以理解的,但在Throwable
的情况下就不行了.
如果我们执行它,我们会得到以下内容:
Exception in thread "main" java.lang.Exception: I am checked exception
at org.mentallurg.Test.doSomething(Test.java:6)
at org.mentallurg.Test.main(Test.java:11)
它是如何工作的?
最重要的部分是这个:
new ThrowableWrapper(throwable).throwNested();
实际上throwNested
这里的方法可以抛出Throwable
.这就是为什么Java编译器应该引发错误,并且应该要求这行代码要么用try/catch括起来,要么应该添加Throwable
子句。但事实并非如此。为什么?我相信这是Java编译器中的一个缺陷。其他线程中关于 SO 的一些注释提到了类型擦除,但它们是不正确的,因为类型擦除在运行时是相关的,正如我们谈论编译时一样。
有趣的是,反编译代码显示这里将抛出一个Throwable
(不是RuntimeException
,不是Error
):
public static <T extends java.lang.Throwable> void throwUnchecked(T);
Code:
0: new #28 // class org/mentallurg/ExceptionUtil$ThrowableWrapper
3: dup
4: aload_0
5: invokespecial #30 // Method org/mentallurg/ExceptionUtil$ThrowableWrapper."<init>":(Ljava/lang/Throwable;)V
8: invokevirtual #32 // Method org/mentallurg/ExceptionUtil$ThrowableWrapper.throwNested:()Ljava/lang/Throwable;
11: pop
12: return
此行为并非特定于Throwable
。我们可以用已检查的Exception
替换它,并将得到相同的结果:
public <T extends Exception> T throwNested() throws T {
在所有这些情况下,如果我们用特定的类替换泛型,那么 Java 编译器将报告一个错误,这是正确的。在泛型的情况下,Java 编译器会忽略检查的异常,并且不会报告任何错误。这就是为什么我认为这是Java编译器中的一个错误。
根据 为什么运行时异常是未经检查的异常?(以及 Jon Skeet ;) 的无可置疑的权威性,似乎 Java 编译器中内置了未经检查的异常支持,并且可能不是我可以插入的内容。
引用乔恩的帖子:
它在规范第 11.1.1 节中明确表示:
RuntimeException 及其所有子类统称为运行时 异常类。
未选中的异常类是运行时异常类和 错误类。
选中的异常类是除未选中的异常类。即,检查的异常类 都是 Throwable 的子类,除了 RuntimeException 及其 子类和错误及其子类。
所以是的,编译器肯定知道RuntimeException。
我仍然希望其他人能想出一个"是的,你可以"的答案,所以我会再等几天再结束这个问题。
这种方法与mentallurg的方法基本相同,但它避免了不必要的包装类。
public final class ThrowUtil {
@SuppressWarnings("unchecked")
private static <T extends Throwable> void throwUnchecked0(Throwable t) throws T {
// because `T` is a generic it will be erased at compile time
// the cast will be replaced with `(Throwable) t` which will always succeed
throw (T) t;
}
public static void throwUnchecked(Throwable t) {
throwUnchecked0(t);
}
private ThrowUtil() {}
}
在您的代码中:
// no throws clause
public void foo() {
ThrowUtil.throwUnchecked(new IllegalAccessException("you are not allowed to call foo"));
}
为了完整起见,以下是生成的字节码进行比较:
private static <T extends java.lang.Throwable> void throwUnchecked0(java.lang.Throwable) throws T;
Code:
0: aload_0
1: athrow
public static void throwUnchecked(java.lang.Throwable);
Code:
0: aload_0
1: invokestatic #1 // Method throwUnchecked0:(Ljava/lang/Throwable;)V
4: return
如果throwUnchecked0
采用类型T
的参数而不是任何Throwable
则对 throws 子句或 try/catch 语句的要求将传播到调用方1。
有趣的是,在大多数情况下,这与泛型的行为非常不同。例如,如果更改了代码,以便内部方法返回T
而不是引发T
,然后公共方法抛出该返回值,则推断T
的类型Throwable
,并且将传播 throws 子句要求。因此,这似乎确实可能是 Java 编译器中的一个错误,尽管我希望看到它仍然存在,因为它仍然可以直接在字节码中实现(并且像 Kotlin 这样的其他 JVM 语言广泛使用),并且在某些情况下可能很有用。
未经检查的强制转换为T
是完全安全的,因为T
在编译时被擦除并替换为Throwable
,这将始终成功,因为参数是Throwable
。
对公共 API 隐藏真正的实现并不是绝对必要的。它只是用于向读者明确(并强制执行)泛型类型旨在从调用站点中完全忽略和省略。
1调用方还可以显式指定泛型参数的类型,并导致传播 throws 子句的要求。这是使用公共包装器方法的部分原因。
2好吧,只是在运行时永远不会失败的意义上。毕竟,我们正在抛出未经检查的异常。