首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
matepi
V2EX  ›  Java

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

  •  
  •   matepi · 41 天前 · 4559 次点击
    这是一个创建于 41 天前的主题,其中的信息可能已经有所发展或是发生改变。

    类似

      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。这点在这个情况下又如何理解呢?

    第 1 条附言  ·  41 天前
    可能底下有挺多同学认为我是想抓了这个异常之后,不去改正,继续运行或者使用;这个想法是不存在的。
    我同样要解决的是“抓到”这个异常之后修正。

    问题是出在:很多人写代码 catch 异常记日志时,只 catch Exception,造成这种 Error 下的 Throwable 不会被“抓到”、从而没有日志。最终异常在实际环境出现时难以通过日志来定位改正。
    26 回复  |  直到 2020-01-17 23:49:07 +08:00
    darrenfang
        1
    darrenfang   41 天前
    这样写并不会有问题

    ```java
    static int i;

    static {
    try {
    i = Integer.parseInt("e");
    } catch (Exception e) {
    }
    }
    ```
    SpencerCJH
        2
    SpencerCJH   41 天前
    学到了
    watzds
        3
    watzds   41 天前 via Android
    非常正常好吧,解析失败,i 没有值,初始化 i 失败,是两种错误
    passerbytiny
        4
    passerbytiny   41 天前
    你可以
    `try{
    ` int i = Integer.parseInt("e");
    `}catch ...
    但不可以
    `try{
    ` static int i = Integer.parseInt("e");
    `}catch ...

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

    @palmers 如果有设计的包装这个是可以理解的。但问题就在于这个包装并不是你设计的。也不是前人设计的。是某种异常环境配置下才会出现的尚未设计的事情。
    iFlicker
        8
    iFlicker   41 天前
    我困惑的是为什么在用 static 修饰 变量 的时候会捕获 ExceptionInitializerError 呢?
    具体是在编译过程的词法分析吗? 有没有大佬解答一下~
    chendy
        9
    chendy   41 天前   ❤️ 2
    静态变量要初始化,静态变量初始化不了类加载会有问题,类加载有问题类就用不了,类都用不了代码就跑不起来也就不用考虑去 catch 了,catch 住了也没用
    momocraft
        10
    momocraft   41 天前
    static 变量的求值发生在 class load 时?
    iFlicker
        11
    iFlicker   41 天前
    @iFlicker 理顺了
    szq8014
        12
    szq8014   41 天前
    是同一个异常,都是 NumberFormatException ,只不过后面那个又给包装了一层,直接换成 error 扔出来了,毕竟是 static 的发生在类加载期,初始化失败当然不能继续运行了,抛个 Error 是可期的
    shily
        13
    shily   41 天前
    @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
        14
    passerbytiny   41 天前
    @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
        15
    passerbytiny   41 天前
    @shily #13 你这个对楼主来说不解决问题,楼主大概是在最外层有一个统一的异常处理,并不想在抛异常 /错误的地方进行解决。他大概率会写成这样:
    static int i;
    static {
    try {
    i = Integer.parseInt("e");
    } catch (NumberFormatException nfe) {
    throw new Exception(nfe);
    }
    }

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

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

    再不济直接再类构造里面写个 static 变量判 null,或判默认-1 之类的,再读取之类
    if (svar == -1) svar = readAndParseIntFromProp(...);
    都比
    直接的
    static int svar = readAndParseIntFromProp(...);
    要少点坑
    sagaxu
        20
    sagaxu   41 天前 via Android
    @matepi 我经常 catch throwable,因为某些场景下,甭管什么原因失败的,我可以 fallback,fallback 失败再降级使用。catch throwable 不是洪水猛兽。
    Raymon111111
        21
    Raymon111111   41 天前
    抛出来包装了一下

    试想你写一个获取用户的接口,结果数据库报了个数据库相关的错误,你肯定得转一下啊
    restlessdream
        22
    restlessdream   41 天前
    Java 的 Error 类 doc 第一句就是:
    An {@code Error} is a subclass of {@code Throwable} that indicates serious problems that a reasonable application
    should not try to catch. Most such errors are abnormal conditions.

    如果出现 Error 的异常,这个时候就表明是严重错误,必须要进行解决,而不是通过 catch 来忽略掉,上面很多人说了,类加载期间出现异常,那么这个类就是不可用的,这个时候需要去解决代码 bug,而不是去 catch 来忽略掉。
    matepi
        23
    matepi   41 天前
    @restlessdream 没说抓了忽略掉不解决……说的是由于有人真去信了 Never catch Throwable class,造成异常一路到了最外层,都没人抓。导致日志里都找不到 Error 的异常。导致 bug 的难以定位解决。
    chendy
        24
    chendy   41 天前
    “我同样要解决的是“抓到”这个异常之后修正。“
    不知道什么类型的项目,如果是 crud 类的全局抓 Throwable 打 log 看一眼 stack trace 就能抓,然后改修修改甩锅甩锅…
    matepi
        25
    matepi   41 天前
    @chendy 不会抓 Throwable,只会抓 Exception,是很多人的习惯。相信这楼里面都有超过一半都是。且希望写全局的人知道抓 Throwable 吧。更多时候只能在自己的代码出口上抓个 Throwable,记下日志,然后继续 throw。
    mxalbert1996
        26
    mxalbert1996   40 天前 via Android
    这里 never catch 的 catch 指的是狭义的 catch,即 catch 后吞掉,先 catch 写了日志以后再 throw 算 rethrow 吧。
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   3045 人在线   最高记录 5168   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 14:48 · PVG 22:48 · LAX 06:48 · JFK 09:48
    ♥ Do have faith in what you're doing.