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

144
为什么Java不允许从静态初始化块抛出已检查异常?背后的设计理念是什么?

1
在静态块中,您想在什么情况下抛出什么样的异常? - Kai Huppmann
2
我不想做那样的事情。我只是想知道为什么在静态代码块中必须捕获已检查异常。 - missingfaktor
1
你会如何期望处理已检查异常?如果它让你感到困扰,只需使用 throw new RuntimeException("告知信息", e); 重新抛出捕获的异常即可。 - Thorbjørn Ravn Andersen
20
Java实际上为该情况提供了一个异常类型:http://docs.oracle.com/javase/6/docs/api/java/lang/ExceptionInInitializerError.html - smp7d
1
@smp7d 请查看下面的kevinarpe答案,以及StephenC的评论。这是一个非常酷的功能,但也有陷阱! - Benj
8个回答

132

因为您无法在源代码中处理这些已检查的异常,所以您无法控制初始化过程,也无法从源代码中调用static{}块,以便您可以使用try-catch将其包围。

由于您无法处理已检查异常指示的任何错误,因此决定禁止抛出已检查异常的静态块。

静态块不得抛出已检查异常,但仍允许抛出未检查/运行时异常。但根据上述原因,您也无法处理这些异常。

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


72
实际上,这个答案是不准确的。你可以在静态块中抛出异常。但是你不能让一个已检查的异常从静态块中传播出来。 - Stephen C
16
如果您正在进行动态类加载,则可以处理此异常,使用Class.forName(...,true,...);当然,这不是您经常遇到的事情。 - LadyCailin
2
静态代码块中抛出了 NullPointerException() 异常,这段代码也无法编译通过! - user1679671
6
一个带有静态初始化器并且总是导致异常的类将无法编译(因为为什么要这样做呢?)。将该抛出语句放入if语句中,就可以了。 - Kallja
3
@Ravisha因为在这种情况下,初始化程序没有机会以任何方式正常完成。使用try-catch可能不会抛出println异常,因此初始化程序有机会在没有异常的情况下完成。是异常的无条件结果使其成为编译错误。请参见JLS:https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.7但是,通过在您的情况下添加简单条件,编译器仍然可能被欺骗: static { if(1 < 10) { throw new NullPointerException(); } } - Kosi2801
显示剩余5条评论

77

您可以通过捕获任何受检异常并将其重新抛出为未经检查的异常来解决该问题。 这个未经检查的异常类作为包装器效果很好: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);
        }
    }
}

1
@DK:也许你的Java版本不支持这种类型的catch子句。尝试使用catch(Exception e){代替。 - kevinarpe
6
可以这样做,但这是一个非常糟糕的想法。未经检查的异常会使该类及依赖它的其他类进入无法恢复的“失败”状态,这通常是不可能解决的,而 System.exit(...)(或等效方法)是你唯一的选择。 - Stephen C
1
@StephenC,我们可以认为如果“父”类无法加载,则实际上不需要加载其依赖类,因为您的代码将无法工作吗?您能否提供一些必须加载此类依赖类的情况示例?谢谢。 - Benj
1
如果代码尝试通过Class.forName动态加载它,怎么样呢? - Stephen C

24

它应该看起来像这样(这 不是 有效的Java代码)

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

但是你如何捕获这个广告呢?Checked exceptions 需要捕获。想象一些可能会初始化该类的示例(或者可能不需要因为它已经被初始化),为了引起注意,我将这些示例放在另一个静态初始化器中以展示它所引入的复杂性:

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(它是一个具有“隐藏”静态初始化器的接口)将不得不抛出异常或处理它-在接口中处理异常?最好保留原样。


10
main可以抛出已检查异常。显然这些异常不能被处理。 - Mechanical snail
3
@Mechanicalsnail:有趣的观点。在我对Java的心理模型中,我假设有一个“神奇”的(默认)Thread.UncaughtExceptionHandler附加到运行main()的线程上,它将异常与堆栈跟踪打印到System.err,然后调用System.exit()。最终,这个问题的答案可能是:“因为Java设计者这样说”。 - kevinarpe

10
为什么Java不允许在静态初始化块中抛出已检查异常?
从技术上讲,您可以这样做。但是,在块内必须捕获已检查的异常。
实际的Java限制是不允许将已检查的异常从块中传播出去。
从技术上讲,也可以允许未经检查的异常从静态初始化程序块中传播出去。但是,故意这样做是一个非常糟糕的主意!问题在于JVM本身捕获未经检查的异常,并将其包装并重新抛出为ExceptionInInitializerError。
注意:ExceptionInInitializerError是一个错误而不是常规异常。您不应尝试从中恢复。
在大多数情况下,无法捕获异常:
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,但通常并非如此。 1 - 如果静态块无条件引发未检查的异常,则会发生编译错误。
2 - 您可能通过注册默认的未捕获异常处理程序来拦截它,但这不允许您进行恢复,因为您的“主”线程无法启动。
3 - 如果要恢复失败的类,则需要摆脱加载它们的类加载器。

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

这是为了保护程序员免于编写无法处理的异常代码...因为程序员没有办法编写处理程序。
正如我们所看到的,静态初始化程序中的异常会将典型应用程序变成一个砖头。语言设计者为帮助程序员做出最好的事情是指定已检查的第一种情况1是编译错误。不幸的是,对于未检查的异常来说这样做并不实用。

如果您的代码“需要”在静态初始化程序中引发异常,该怎么办?

基本上,有两个选择:
  • 如果在块内(完全!)从异常中恢复是可能的,请执行该操作。

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


有没有关于如何构建代码以避免静态初始化的一般建议? - MasterJoe
这些解决方案听起来怎么样?https://dev59.com/HXvaa4cB1Zd3GeqPGK7c#21321935 和 https://dev59.com/A3I95IYBdhLWcg3w-DHz#56575807 - MasterJoe
2
  1. 我没有任何东西。
  2. 它们听起来很糟糕。请看我在它们上面留下的评论。但我只是在重复我在上面回答中所说的话。如果你阅读并理解我的回答,你就会知道那些“解决方案”不是真正的解决方案。
- Stephen C

4
请查看Java语言规范:如果静态初始化器失败 能够通过检查异常突然完成,则会在编译时发生错误。

5
但是这并没有回答问题。他问的是为什么它是编译时错误。 - Winston Smith
嗯,因为JLS只提到了已检查的异常,所以抛出任何RuntimeError应该是可能的。 - Andreas Dolk
没错,但你永远不会看到它作为堆栈跟踪。这就是为什么在静态初始化块中需要小心。 - EJB
2
@EJB:这是不正确的。我刚试了一下,以下代码给了我一个可视化的堆栈跟踪:public class Main { static { try{Class.forName("whathappenswhenastaticblockthrowsanexception");} catch (ClassNotFoundException e){throw new RuntimeException(e);} } public static void main(String[] args){} }输出:Exception in thread "main" java.lang.ExceptionInInitializerError Caused by: java.lang.RuntimeException: java.lang.ClassNotFoundException: whathappenswhenastaticblockthrowsanexception at Main.<clinit>(Main.java:6) Caused by: java.lang.ClassNotFoundException: whathappen... - Konrad Höffner
"Caused by" 部分显示的是您可能更感兴趣的堆栈跟踪。 - LadyCailin

2

由于你编写的任何代码都不能调用静态初始化块,因此抛出受检查的 异常 没有用处。如果可能的话,当抛出受检查的异常时,jvm会怎样处理呢?运行时异常 会向上传播。


1
嗯,是的,我现在明白了。我发这样一个问题真的很愚蠢。但是...现在我不能删除它了。 :( 尽管如此,还是要给你的回答点个赞... - missingfaktor
1
@fast,实际上,已检查异常并没有转换为RuntimeExceptions。如果您自己编写字节码,可以在静态初始化器中随意抛出已检查的异常。JVM根本不关心异常检查;它纯粹是Java语言的构造。 - Antimony

0
例如: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());
    }

2
这种方法解决了无法捕获未检查异常的问题。相反,它会将该类及其依赖的任何其他类置于不可恢复的状态。 - Stephen C
@StephenC - 你能否举个简单的例子,说明我们何时需要一个可恢复的状态? - MasterJoe
1
假设...如果你想要能够从IOException中恢复,以便应用程序可以继续运行。如果你想这样做,那么你必须捕获异常并实际处理它...而不是抛出未经检查的异常。 - Stephen C

-5

我能够编译并抛出一个已检查的异常,同时……

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

3
是的,但你是在静态块中捕获它。你不可以从静态块内部抛出已检查异常到外部。 - ArtOfWarfare

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