Java 的有个异常设计一直没理解

2020-01-17 09:15:24 +08:00
 matepi

类似

  int i = Integer.parseInt("e");
会抛出
  java.lang.NumberFormatException
  NumberFormatException 的隶属关系
Object
  -Throwable
    -Exception
      -RuntimeException
        -IllegalArgumentExcetipn
          -NumberFormatException

挺正常,捕获 Exception 就能 catch 住他

然而如果这么写

  static int i = Integer.parseInt("e");
会抛出
  java.lang.ExceptionInitializerError ..
 Caused by: java.lang.NumberFormatException ...
而 ExceptionInitializerError 的的隶属关系
Object
  -Throwable
    -Error
      -LinkageError
        -ExceptionInitializerError

这时,捕获 Exception 已经不能 catch 住这个异常了。需要捕获 Throwable 或者 Exception | Error 了。

然而同样都是做了一个Integer.parseInt("e"),这时升格成为 static,就造成了异常类的变化。导致捕获方式的变化。这是不是很奇怪?

当然这里的Integer.parseInt("e"),只是一个例子 如果你有你可爱的同学,在 static 块 /变量里面写了一大段复杂逻辑,搞出各种各样的 Exception,都会被升格成为ExceptionInitializerError。从而不能被 catch Exception 所 catch

从类的初始化过程、从编译的角度貌似能够理解这个 LinkageError。但从具体逻辑的角度,这为啥呢?

其实这个还有一个问题就是有人说到的所谓的异常处理"军规",Never catch Throwable class。这点在这个情况下又如何理解呢?

6697 次点击
所在节点    Java
26 条回复
darrenfang
2020-01-17 09:45:01 +08:00
这样写并不会有问题

```java
static int i;

static {
try {
i = Integer.parseInt("e");
} catch (Exception e) {
}
}
```
SpencerCJH
2020-01-17 09:48:18 +08:00
学到了
watzds
2020-01-17 09:52:09 +08:00
非常正常好吧,解析失败,i 没有值,初始化 i 失败,是两种错误
passerbytiny
2020-01-17 09:54:17 +08:00
你可以
`try{
` int i = Integer.parseInt("e");
`}catch ...
但不可以
`try{
` static int i = Integer.parseInt("e");
`}catch ...

所以前者抛出异常,后者抛出错误。Exception、Error 虽然都继承自 Throwable,但原则上前者捕获后允许处理使其消失,后者捕获后只能做日志、告警而不能处理——必须中断程序或者 5**返回。
palmers
2020-01-17 09:56:13 +08:00
我同意 3#的说法 两种异常一种是解析失败异常一种是初始化失败异常, 只不过后面又给包装了一下 就像我们常常将 api 的异常包装为自己的业务异常一样
lff0305
2020-01-17 10:01:06 +08:00
ExceptionInitializerError 初始化异常, 表示初始化这个类的时候出错了,至于为什么出错,具体原因是 NumberFormatException, 在 getCause() 里面
matepi
2020-01-17 10:13:17 +08:00
@darrenfang @passerbytiny 有的时候,我们是面向别人的代码编程……你引入了一个包,甚至都没有源码的情况下。并不知道可爱的前任给你留了个这样的坑。
说真的,类似这种初始过程中异常性没完全包好的事情,其实是很多的,即便质量比较好的框架有时候也会和一些环境配置上搞出这样的事情。最终还会出一些类似类明明在,然后 NoClassDefFound 的异常。又类似 headless 那种环境性配置。

@palmers 如果有设计的包装这个是可以理解的。但问题就在于这个包装并不是你设计的。也不是前人设计的。是某种异常环境配置下才会出现的尚未设计的事情。
iFlicker
2020-01-17 10:34:25 +08:00
我困惑的是为什么在用 static 修饰 变量 的时候会捕获 ExceptionInitializerError 呢?
具体是在编译过程的词法分析吗? 有没有大佬解答一下~
chendy
2020-01-17 10:35:10 +08:00
静态变量要初始化,静态变量初始化不了类加载会有问题,类加载有问题类就用不了,类都用不了代码就跑不起来也就不用考虑去 catch 了,catch 住了也没用
momocraft
2020-01-17 10:38:51 +08:00
static 变量的求值发生在 class load 时?
iFlicker
2020-01-17 10:57:33 +08:00
@iFlicker 理顺了
szq8014
2020-01-17 11:04:12 +08:00
是同一个异常,都是 NumberFormatException ,只不过后面那个又给包装了一层,直接换成 error 扔出来了,毕竟是 static 的发生在类加载期,初始化失败当然不能继续运行了,抛个 Error 是可期的
shily
2020-01-17 11:18:16 +08:00
@iFlicker
@lff0305 # 6 的才是正确答案。

『都会被升格成为 ExceptionInitializerError。从而不能被 catch Exception 所 catch 』这个说法本身是错误的。
呃,怎么说呢,它并不是被升格,而是因为你不处理,导致 JVM 无法进行下去,JVM 崩溃了,为了进一步表示 JVM 为什么崩溃而包装了一下。

ExceptionInitializerError 表示类初始化的时候发生了异常,可以通过 getCause() 获知错误的具体原因。

呐如果我想自己处理 NumberFormatException 改怎么写呢?

static int i = Integer.parseInt("e"); // 无法直接使用 try-catch

但是它等价于

static int i;
static {
i = Integer.parseInt("e");
}


这样你就可以自行处理其中抛出的异常了。
static int i;
static {
try {
i = Integer.parseInt("e");
} catch (NumberFormatException nfe) {
// handle exception here
}
}
passerbytiny
2020-01-17 11:32:27 +08:00
@matepi #7 先讲技术,再找理由。

int i = Integer.parseInt(some 变量)才有可能出现异常,而在非演示目的的情况下写出来 int i = Integer.parseInt("e")这样的代码就是沙雕,所以此时抛出的是运行时异常——你无法避免它但是可以解决它。
static int i = Integer.parseInt("e")这明显是沙雕代码; static String someIntStr = "e"; static int i = Integer.parseInt( someIntStr )仍然是沙雕代码; static int i; void initOrUpdate(String aIntStrForInit){ if(Class.i == -1){ Class.i = Integer.parseInt(aIntStrForInit)} }会抛出 NumberFormatException:所以此时前两种情况抛出的是错误——你不能解决它而只能修改源代码区避免它

技术上来说:
一,框架或者库,不管是沙雕还是没考虑到而发生了致命错误,就该给对方抛出个 java.lang.Error,告诉对方这玩意你别解决了,等我解决或者用其它库。而不能是抛出个 java.lang.Exception 或者 java.lang.RuntimeException,告诉对方这玩意你自行解决或者不予理会。作为对方,你应该感谢对方抛了 Error 通知你,而不是抛个 Exception 掩盖致命错误。
二,前人的代码,不管是沙雕、不想干,还是什么乱起八糟的原因,发生致命错误的时候,就该给后人抛出个 java.lang.Error,告诉后人这玩意你别想在你自己的代码中解决了,重构我给你留的代码或者向上级撂挑子吧。作为后人,你应该感谢前人好心提醒你,而不是弄个 Exception 继续糊弄你。

技术上来说还可以简单的一句话:坑就在那里,你要过去就必须填坑;你把 Error 换成 Exception 并不能填坑,反而让你掉进去。

以上大概能解决你对“Never catch Throwable class”的疑问。

下面来说 static int i = Integer.parseInt("e");抛出 Error。

静态变量不是 new 对象的时候由 ClassLoader 设值的,而是 JVM 看到它的时候由 JVM 设值的,此时要是无法设值,那就是 JVM 级别的致命错误。你就算不考虑底层实现,在上层上看,给静态值设置值却设不上去,就像给人取名叫张三但“张三”这两个字有脾气一样,是致命错误。static SomeType someVariable = {一段有可能出错的代码},这是很危险的行为,抛出 Error 不足为怪。
passerbytiny
2020-01-17 11:42:46 +08:00
@shily #13 你这个对楼主来说不解决问题,楼主大概是在最外层有一个统一的异常处理,并不想在抛异常 /错误的地方进行解决。他大概率会写成这样:
static int i;
static {
try {
i = Integer.parseInt("e");
} catch (NumberFormatException nfe) {
throw new Exception(nfe);
}
}

然后编译都过不去,回过头来再表达“我很苦”。

@iFlicker #8 原则上是不能捕获 Error 的,他应该是在最上层捕获了 Throwable,或者不是捕获到而是程序中断后在日志中看到。
dallaslu
2020-01-17 11:51:39 +08:00
V2EX 的评论里到底能不能用 markdown 呀?
matepi
2020-01-17 12:52:51 +08:00
@passerbytiny 不,并不会是想在外层解决。这个问题之所以提,是就是因为这是没法很好在外层解决的。必须改内层他人代码的“穿层”的问题。
有问题能查就行,这种异常麻烦就加之大家都不捕获 exception 以外的 throwable 来记日志。
这个 parse"e"只是个简写例子,不是真傻代码。真情况是环境上遇到过 parseInt 一个配置文件里面读上来的字符串。然后配置字符串该写数字 0 的,写了个字母 O。然后解析失败。error 异常一路跑到最外层,最外层的 logger 只捕获 exception。没有默认 err 的 log,异常在所有的日志里都没有。结果这个问题就变得异常的难查明。
lazyfighter
2020-01-17 13:11:16 +08:00
理解一下 error 和 exception 我感觉就可以明白了
matepi
2020-01-17 13:22:05 +08:00
如果大家要写类似在初始化中的代码
首先还是推荐大家搞工厂单例

再不济直接再类构造里面写个 static 变量判 null,或判默认-1 之类的,再读取之类
if (svar == -1) svar = readAndParseIntFromProp(...);
都比
直接的
static int svar = readAndParseIntFromProp(...);
要少点坑
sagaxu
2020-01-17 13:30:19 +08:00
@matepi 我经常 catch throwable,因为某些场景下,甭管什么原因失败的,我可以 fallback,fallback 失败再降级使用。catch throwable 不是洪水猛兽。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/638609

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX