为什么 Java 不允许从静态初始化块抛出检查异常?

为什么 Java 不允许从静态初始化块抛出检查异常?这个设计决定背后的原因是什么?

94771 次浏览

看一下 Java 语言规范: 它指出,如果静态初始化器 失败 能够突然完成带有检查的异常,那么这是一个编译时错误。

因为不可能在源代码中处理这些检查过的异常。您对初始化过程没有任何控制权,并且不能从源调用静态{}块,因此可以用 try-catch 包围它们。

由于无法处理由已检查异常指示的任何错误,因此决定不允许引发已检查异常的静态块。

静态块不能抛出 检查过了异常,但仍然允许抛出未检查的/运行时异常。但根据以上原因,你也无法处理这些问题。

总而言之,这种限制阻止(或至少使开发人员更难)构建可能导致应用程序无法恢复的错误。

它必须看起来像这样(这是 没有有效的 Java 代码)

// Not a valid Java Code
static throws SomeCheckedException {
throw new SomeCheckedException();
}

但如何广告,你在哪里接住它?检查异常需要捕获。想象一些例子可能会初始化这个类(或者可能不会,因为它已经初始化了) ,为了引起注意,我把这些例子放在另一个静态初始化程序中:

static {
try {
ClassA a = new ClassA();
Class<ClassB> clazz = Class.forName(ClassB.class);
String something = ClassC.SOME_STATIC_FIELD;
} catch (Exception oops) {
// anybody knows which type might occur?
}
}

还有一件恶心的事

interface MyInterface {
final static ClassA a = new ClassA();
}

假设 ClassA 有一个静态初始化器抛出一个已检查的异常: 在这种情况下,MyInterface (这是一个带有“隐藏”静态初始化器的接口)必须抛出异常或者处理它——在接口上进行异常处理?最好就这样吧。

由于您编写的任何代码都不能调用静态初始化块,因此抛出选中的 exceptions是没有用的。如果可能的话,当抛出检查过的异常时,jvm 会做什么?Runtimeexceptions向上传播。

您可以通过捕获任何选中的异常并将其作为未选中的异常重新引发来解决这个问题。这个未检查的异常类作为包装器工作得很好: java.lang.ExceptionInInitializerError

示例代码:

protected static class _YieldCurveConfigHelperSingleton {


public static YieldCurveConfigHelper _staticInstance;


static {
try {
_staticInstance = new YieldCurveConfigHelper();
}
catch (IOException | SAXException | JAXBException e) {
throw new ExceptionInInitializerError(e);
}
}
}

我能够编译抛出一个选中的异常。

static {
try {
throw new IOException();
} catch (Exception e) {
// Do Something
}
}

为什么 Java 不允许从静态初始化块抛出检查异常?

从技术上讲,您可以这样做。但是,检查的异常必须在块内捕获。

实际的 Java 限制是不允许对 繁殖执行异常检查。

从技术上讲,也可以允许 不受约束异常从静态初始化程序 lock1中传播出去。但是故意这样做真的是一个很糟糕的主意!问题在于 JVM 本身捕获未检查的异常,并将其包装并以 ExceptionInInitializerError的形式重新抛出。

注意: ExceptionInInitializerErrorError,不是常规异常。不要试图从中恢复。

在大多数情况下,无法捕捉到例外情况:

public class Test {
static {
int i = 1;
if (i == 1) {
throw new RuntimeException("Bang!");
}
}
    

public static void main(String[] args) {
try {
// stuff
} catch (Throwable ex) {
// This won't be executed.
System.out.println("Caught " + ex);
}
}
}


$ java Test
Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.RuntimeException: Bang!
at Test.<clinit>(Test.java:5)

没有地方,你可以把一个 try ... catch在上面赶上 ExceptionInInitializerError2

在某些情况下,你可以抓住它。例如,如果通过调用 Class.forName(...)触发了类初始化,则可以将调用封装在 try中,并捕获 ExceptionInInitializerError或随后的 NoClassDefFoundError

然而,如果你试图从 ExceptionInInitializerError恢复,你很可能会遇到路障。问题是,在抛出错误之前,JVM 将导致问题的类标记为“失败”。你根本无法使用它。此外,依赖于失败类的任何其他类如果尝试初始化也将进入失败状态。前进的唯一方法是卸载所有失败的类。对于动态加载的代码 3来说,也许吧是可行的,但是通常情况下它是不可行的。

如果静态块 无条件的抛出未检查的异常,则为编译错误。
2-你的 也许吧能够通过注册一个默认的未捕获的异常处理程序来拦截它,但是这不允许你恢复,因为你的“主”线程不能启动。
3-如果你想恢复失败的类,你需要摆脱装载它们的类加载器。


这个设计决定背后的原因是什么?

它是为了防止程序员编写代码时抛出无法处理的异常... ... 因为程序员没有办法编写处理程序。

正如我们所看到的,静态初始值设定项中的异常会将典型的应用程序变成一块砖。语言设计人员能够帮助程序员做的最好的事情就是指定被检查的用例1是一个编译错误。不幸的是,对于未检查的异常也这样做是不切实际的。


如果您的代码“需要”在静态初始化程序中抛出异常,您应该怎么做?

基本上,有两种选择:

  • 如果可以从异常 在街区内中恢复(完全恢复!) ,那么就这样做。

  • 否则,重新构造代码,使初始化不会发生在静态初始化块(或静态变量的初始化器)中。将初始化放在可以从常规线程调用的方法或构造函数中。

例如: Spring 的 DispatcherServlet (org.springframework.web.servlet. DispatcherServlet)处理捕获已检查异常并抛出另一个未检查异常的场景。

static {
// Load default strategy implementations from properties file.
// This is currently strictly internal and not meant to be customized
// by application developers.
try {
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
}