Java异常详解

异常 Exception,错误 Error

异常Exception,错误Error

既然是错误(Exception,Error),那就应该被解决。

既然被抛出(Throwable),那就应该被接住。

异常/错误处理机制

对于 Java 程序 来说,出现异常 / 错误,有以下情况

  1. 出现异常 Exception ,两种解决方案。①谁弄出来的谁自己内部解决 ②自己解决不了,找自己的老大(上层—调用该出错方法的方法),把难题递给老大,让它去解决。
  2. 出现错误 Error ,两种情况。①错误有可能解决,抛出错误会被接住并处理 ②错误解决不了,抛出异常没人接住,最后递交给 JVM,JVM 打印出错信息并关闭系统。

异常处理原理简述

JVM 的虚拟机栈在每次方法调用时创建对应栈帧并压栈。当正在执行的方法(位于栈顶的栈帧)抛出异常 throw,那么就依次将栈顶元素弹出,直到找到对应与抛出异常类型相符的异常处理器 catch。如果栈空且没找到合适的处理器,那么就意味着将异常扔给 JVM,即打印异常信息并退出程序。

Exception

javadoc:

* The class {@code Exception} and its subclasses are a form of
* {@code Throwable} that indicates conditions that a reasonable
* application might want to catch.
*
* <p>The class {@code Exception} and any subclasses that are not also
* subclasses of {@link RuntimeException} are <em>checked
* exceptions</em>.  Checked exceptions need to be declared in a
* method or constructor's {@code throws} clause if they can be thrown
* by the execution of the method or constructor and propagate outside
* the method or constructor boundary.

java 将 Exception 大致分为两类。RuntimeException,非 RuntimeException。

Exception 及其非RuntimeException 子类统称为受检异常。

CheckedException 受检异常

Java 强制抛出受检异常需要在方法内进行捕获并处理。或者当前方法声明抛出该异常,交由调用当前方法的方法来处理该异常。

有的受检异常发生了,如果代码无法做出什么来补救,因为 Java 的强制规定,还是得有人 catch 捕获受检异常。这就很蛋疼。

那我们能做的就是 catch 这个异常并记录它或者将它包装为 RuntimeException 并抛出。

重新抛出异常时,一定要设置异常原因避免丢失异常信息。异常链

public static void main(String[] args) {
        try {
            test();
        } catch (IOException e) {
            RuntimeException runtimeException = new RuntimeException("异常信息...");
            runtimeException.initCause(e);
            throw runtimeException;
        }
    }

Spring 意识到了 chechException 的蛋疼之处,Spring 建议不要在 Controller、Service 层处理异常,而是抛出后由全局异常处理类 ControllerAdvice 进行统一处理。

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(value = xxxException.class)
	public String exceptionHandler(Exception e){
		logger.error("...",e);
       	return ...;
    }
}

RuntimeException 运行时异常

抛出运行时异常不强制 try catch ,甚至不建议在方法中 try catch。

阿里代码规范指出,对于抛出 RuntimeException 应当予以检查并规避,而不是使用 try catch 包裹。

  1. 【强制】 Java 类库中定义的一类 RuntimeException 可以通过预先检查进行规避,而不应该

    通过 catch 来处理,比如: IndexOutOfBoundsException , NullPointerException 等等。

    说明:无法通过预检查的异常除外,如在解析一个外部传来的字符串形式数字时,通过 catch

    NumberFormatException 来实现。

    正例: if (obj != null) {…}

    反例: try { obj.method() } catch (NullPointerException e) {…}

  2. 【推荐】定义时区分 unckecked / checked 异常,避免直接使用 RuntimeException 抛出,更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常.推荐业界或者集团已定义过的自定义异常,如:DaoException / ServiceException 等.

Error

抛出 Error 不强制捕获或声明,因为不指望 Java 程序能修复 Error。

大部分 Error 无法解决,因为都与 JVM 有关。例如最常见的内存溢出导致 OOM、StackOverflowError、GC 错误导致 OOM,都无法在程序运行期间解决。

但"Never say never",部分 Error 还是可以进行解决的

名字相似的 Exception ,Error

例如 IllegalAccessException 与 IllegalAccessError。

IllegalAccessException 是当程序运行期间,我们通过反射获取Field/Method/Constructor实例后,想要操作却没有访问权限,违背了Java封装性而抛出的异常。

但 IllegalAccessError 却大不相同,编译期间类 C 能够找到并调用其他类的 A 方法,但运行期间发现类 C 没有访问 A 方法 的权限。这时虚拟机发现可能是出现了严重错误,编译期间与运行期间结果不同,抛出 IllegalAccessError 。

其他名字相似的例如:

IllegalAccessException / IllegalAccessError
InstantiationException / InstantiationError
ClassNotFoundException / NoClassDefFoundError
NoSuchFieldException / NoSuchFieldError
NoSuchMethodException / NoSuchMethodError

参考

https://stackoverflow.com/questions/3074635/difference-between-illegalaccesserror-and-illegalaccessexception

https://stackoverflow.com/questions/7076414/java-lang-illegalaccesserror-tried-to-access-method

抛出 throw 、throws

throw 主动抛出异常

当可以通过代码检测出程序出错的话,可以通过抛出异常快速终止该方法,让高层去处理异常。

浅谈 fail-fast 机制

实例代码:

public void test throws Exception(int a,int b){
    if(b == 0){
        throw new Exception("除数为0")
    }else{
        a = a / b;
    }
}

通过代码可以判断出程序出错时,可以通过抛出异常快速结束程序。

java.util 包下的集合类都是此机制,它们不保证线程安全。例如 ArrayList##Itr::foreach(),HashMap##EntrySet::foreach(),

 if (modCount != mc)
     throw new ConcurrentModificationException();

判断遍历时 modCount (修改次数) 是否被修改,被修改则抛出异常快速终止遍历。

当然我们不希望因为集合遍历出错而终止当前方法,如何避免?

浅谈 fail - safe 机制

java.util.concurrent 包下的集合类都是此机制,保证线程安全,它们永远不会抛出 ConcurrentModificationException 导致程序终止。例如 CopyOnWriteArrayList、ConcurrentHashMap

fail-safe 的代价是修改时需要复制底层数组,不影响原数组被读。从而增大了内存开销,降低了性能。

throws 主动声明异常

方法声明异常也是多态的一种体现

public void throwTest(String str) throws ClassNotFoundException,FileNotFoundException{
        Class clazz = Class.forName("123");
        throw new FileNotFoundException();
}
--------------------------------------------------
public void throwTest(String str) throws Exception{
        Class clazz = Class.forName("123");
        throw new FileNotFoundException();
}

这两个方法都可以正常编译,因为可以跑出子类异常,方法声明其父类异常。但更推荐第一种方式,两个异常可以分开 catch,更加具体。

方法声明的异常算在 Java 方法特征签名中吗?

光说不练假把式,直接代码试一试。

public void throwTest(String str) throws Exception{
    throw new FileNotFoundException();
}

public int throwTest(String str) throws ArithmeticException{
        throw new ArithmeticException();
}

编译不通过,gg。方法声明的异常不算在 Java 方法特征签名中。

其实就连方法返回值也不算在方法特征签名中。细心的同学可以看到两个 throwTest 方法的返回值不同,Java 却还是不允许这样。那么 Java 规定的方法特征签名包含什么呢?

其实就俩个:1.方法简单名称 (throwTest) 2.方法参数列表 (String str)

但以上两个方法,在 class 文件中是被允许的。因为在 Class 文件格式中,方法特征签名的范围更大一些。

1.方法简单名称 (throwTest) 2.方法参数列表 3.方法返回值

方法声明异常与代码复用

在接口中定义一个方法

public interface Test{
	void test() throws xxxException,xxxException,xxxException,xxxException,xxxException;
}

这样一看就很蛋疼,如果 test() 方法需要进行修改,修改完有一个受检异常不用抛了。那岂不是不光接口需要修改,还有所有调用 test() 的方法都得修改方法中的 try - catch,删掉多余的 catch 块。

所以方法声明异常为代码复用增加了难度。为了代码复用,可能抛出运行时异常更加适合。

接住(捕获) catch

catch{} :出现可以捕捉的异常时,执行的代码块

catch Exception

如何正确捕获并记录异常信息

  1. 捕获受检异常并抛出非受检异常并交由上层处理
try {
    ...
} catch (IOException e) {
    throw new RuntimeException("异常信息...",e);
}
  1. 记录异常信息
try {
    ...
} catch (IOException e) {
    logger.error("异常信息...,{}",e);
}

catch Error

众所周知 catch(Exception e){} 可以捕获 Exception 及其子类的异常,那么 catch(Throwable t){} 可以捕获其子类 Error 吗?

When to catch java.lang.Error?

部分 Error 例如 OOM,捕获了也没啥用,所以 Java 建议不要捕获 Error。但 Never say never ,有的情况还是需要捕获的 例如 LinkageError 类的部分子类。

在此给出两个 LinkageError 类的子类可能发生的情况,博主水平不高无法给出具体解决方案,只能贴上链接…

NoSuchMethodError

  • 可能出错情况

两个 jar 包,两个类C相同类名相同报名。jar包A 的类C 有 func 方法,jar包B的类C 没有。编写代码时,调用 jar包A 类 C 的 func() 方法。在编译期间先加载了 jar包B,JVM 在 jar包B 中找不到目标 C::func(),抛出 NoSuchMethodError。

  • 解决: https://www.jianshu.com/p/853a93aa5b38:

IllegalAccessError

  • 可能出错情况

jar 包 J1 中旧版本的类 C 有私有方法 A,jar 包 J2 中新版本的类 C 的 A 方法非私有。写代码时,在类 C1 中调用 jar包 J2 中的 C::A() 方法。如果类加载期间,jar 包 J1 先被加载。(我们知道类加载器是通过类的全限定名寻找描述类的二进制字节流的,两个 jar 包中的 类C 全限定名相同。如果又恰好两个类是由同一个类加载器加载的。那么当类加载器加载 J1 中的类 C 后,就不会再去加载 J2 中的类C 了。 同一个类只加载一次)

当运行到 C1 中的方法,JVM 通过C的全限定名找到了 jar包J1 中的 C::A() 方法。发现与编译期间不同,这个方法是私有的,类C1 没有访问 C::A() 的权限。于是认为是很严重的错误,抛出 IllegalAccessError。

静态代码块不能抛出检查异常

静态代码块不能抛出检查异常,你可以捕获检查的异常,适当地记录它并改为抛出运行时异常。您需要将检查的异常嵌套为根本原因。

异常链

有时候需要捕获异常并以更高级的异常重新抛出。抛出时如果不注册异常原因,则会丢失异常信息。

public class ThrowableLinkRoad {
    /**
     * 打印异常
     */
    public static void testThrow(){
        try {
            testThrow1();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 异常链路
     */
    public static void testThrow1(){
        try {
            testThrow2();
        } catch (Exception e) {
            throw new RuntimeException("testThrow2 抛出异常");
        }
    }

    /**
     * 产生异常
     * @throws Exception
     */
    public static void testThrow2() throws Exception {
        try {
            int i = 1/0;
        } catch (ArithmeticException e) {
            Exception exception = new Exception("除零",e);
            throw exception;
        }
    }

    public static void main(String[] args) {
        testThrow();
    }
}

//output
java.lang.RuntimeException: testThrow2 抛出异常
    at exception.ThrowableLinkRoad.testThrow1(ThrowableLinkRoad.java:29)
	at exception.ThrowableLinkRoad.testThrow(ThrowableLinkRoad.java:15)
	at exception.ThrowableLinkRoad.main(ThrowableLinkRoad.java:47)

testThrow1() 方法丢失异常信息,创建新的异常对象时,需要设置原异常原因。Throwable::initCause()

修改 testThrow1()

public static void testThrow1(){
        try {
            testThrow2();
        } catch (Exception e) {
            throw new RuntimeException("testThrow2 抛出异常",e);
        }
    }

//output
java.lang.RuntimeException: testThrow2 抛出异常
	at exception.ThrowableLinkRoad.testThrow1(ThrowableLinkRoad.java:29)
	at exception.ThrowableLinkRoad.testThrow(ThrowableLinkRoad.java:15)
	at exception.ThrowableLinkRoad.main(ThrowableLinkRoad.java:47)
Caused by: java.lang.Exception: 除零
	at exception.ThrowableLinkRoad.testThrow2(ThrowableLinkRoad.java:41)
	at exception.ThrowableLinkRoad.testThrow1(ThrowableLinkRoad.java:27)
	... 2 more
Caused by: java.lang.ArithmeticException: / by zero
	at exception.ThrowableLinkRoad.testThrow2(ThrowableLinkRoad.java:39)
	... 3 more

finally

不论是否出现异常,都要执行的代码块。执行完 finally 代码块,才能结束方法。

finally 与 释放资源

我们经常在 finally 中关闭IO流或者释放手动锁,阿里巴巴规范也是这么规定的:

【强制】finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch

jdk7 前,关闭流。需要在 finally 中 close,嵌套 try-catch,非常冗余。

public class ResourceCloseTest {
    public static void main(String[] args) {
        InputStream inputStream = null;
        OutputStream outputStream = null;
        BufferedInputStream bufferedInputStream = null;
        BufferedOutputStream bufferedOutputStream = null;
        try {
            inputStream = new FileInputStream("123");
            bufferedInputStream = new BufferedInputStream(inputStream);
            outputStream = new FileOutputStream("123");
            bufferedOutputStream = new BufferedOutputStream(outputStream);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                inputStream.close();
                bufferedInputStream.close();
                outputStream.flush();
                outputStream.close();
                bufferedOutputStream.flush();
                bufferedOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

jdk7 后,将 close() 方法抽象为一个接口 AutoCloseable。try -with -resources 语法糖自动调用 close() 方法。

public class ResourceCloseTest {
    public static void main(String[] args) {
        try(
                InputStream inputStream = new FileInputStream("123");
                BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
                OutputStream outputStream = new FileOutputStream("123");
                BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)
                )
        {
            //...
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

javac 编译器解糖后,代码为:

public class ResourceCloseTest {
    public ResourceCloseTest() {
    }

    public static void main(String[] var0) {
        try {
            FileInputStream var1 = new FileInputStream("123");
            Throwable var2 = null;

            try {
                BufferedInputStream var3 = new BufferedInputStream(var1);
                Throwable var4 = null;

                try {
                    FileOutputStream var5 = new FileOutputStream("123");
                    Throwable var6 = null;

                    try {
                        BufferedOutputStream var7 = new BufferedOutputStream(var5);
                        Object var8 = null;
                        if (var7 != null) {
                            if (var8 != null) {
                                try {
                                    var7.close();
                                } catch (Throwable var61) {
                                    ((Throwable)var8).addSuppressed(var61);
                                }
                            } else {
                                var7.close();
                            }
                        }
                    } catch (Throwable var62) {
                        var6 = var62;
                        throw var62;
                    } finally {
                        if (var5 != null) {
                            if (var6 != null) {
                                try {
                                    var5.close();
                                } catch (Throwable var60) {
                                    var6.addSuppressed(var60);
                                }
                            } else {
                                var5.close();
                            }
                        }

                    }
                } catch (Throwable var64) {
                    var4 = var64;
                    throw var64;
                } finally {
                    if (var3 != null) {
                        if (var4 != null) {
                            try {
                                var3.close();
                            } catch (Throwable var59) {
                                var4.addSuppressed(var59);
                            }
                        } else {
                            var3.close();
                        }
                    }

                }
            } catch (Throwable var66) {
                var2 = var66;
                throw var66;
            } finally {
                if (var1 != null) {
                    if (var2 != null) {
                        try {
                            var1.close();
                        } catch (Throwable var58) {
                            var2.addSuppressed(var58);
                        }
                    } else {
                        var1.close();
                    }
                }

            }
        } catch (FileNotFoundException var68) {
            var68.printStackTrace();
        } catch (IOException var69) {
            var69.printStackTrace();
        }

    }
}

javap - ExceptionTable 解析 try-catch-finally 的执行顺序

public static int test1(){
    int i = 0;
    try {
        i = 1;
        return i;
    } catch (Exception e) {
        i = 3;
        return i;
    } finally {
        i = 2;
        System.out.println(i); 
    }
}

光通过看代码是猜不出来执行顺序的。只有运行它才知道。

不出现异常时,test1() 方法返回 1。出现 Exception 及其子类异常时,test1() 方法返回 3。

有同学可能有疑问了,为什么不返回 2 呢。当 finally 块不存在吗?

提出疑问解决疑问,实际上,通过运行代码我们也无法看清代码的执行顺序。

这时候,javap 反编译说他可以汪汪队出动!

这里给出结论,ireturn 字节码指令,athrow 字节码指令作为 test1() 方法的结束指令。要被 return 方法返回值及要被 throw 的异常对象以局部变量的形式存在于当前方法栈帧的局部变量表中。 test1() 方法中存在多条 return 字节码指令,每条 return 指令对应一个方法返回值。未出现异常,方法返回值为 1。出现 Exception 及其子类异常,方法返回值为 3。finally 块只是修改了局部变量表中局部变量 i 的值,并没有改变真正的方法返回值。或者可以说,方法返回值在运行到 finally 块前就已经被确定了。

详细原理看我之前的博客:方法返回值原理: 解析Java中参数的传递方式与方法返回值方式

所以,test1() 方法不会返回 2。除非修改代码,在 finally 块中加入 return i 语句。

但这又会引起一个漏洞:finally + return 漏洞。往下看…

finally + return 漏洞

阿里巴巴规范中也指出了:

在这里插入图片描述

因为 finally 必执行。所以 finally 中如果有 return 的话,方法返回值被 finally 确定。try,catch 块中不论是否有 return 、throw语句或者需要捕获的异常信息,都没用。

例如以下代码:正常情况下返回 1,出现 Exception 及其子类异常情况下返回 3

public static int test1(){
    int i = 0;
    try {
        i = 1;
        return i;
    } catch (Exception e) {
        i = 3;
        return i;
    } finally {
        i = 2;
        System.out.println(i); 
    }
}

如果在 finally 块中加入 return。则不论什么情况,返回值都为 2:

public static int test1(){
    int i = 0;
    try {
        i = 1;
        return i;
    } catch (Exception e) {
        i = 3;
        Class.forName("我是异常你来抓我啊~");
        return i;
    } finally {
        i = 2;
        System.out.println(i); 
    }
}

就连 catch 块中的检查异常 ClassNotFoundException 也"不需要"检查了。

学废自定义异常 Exception

自定义异常,即选择一个父类继承。RuntimeException / Exception

public class MyException extends RuntimeException {    public MyException(){        super();    }    public MyException(String message){        super(message);    }    public MyException(String message,Throwable cause){        super(message,cause);    }    public MyException(Throwable cause){        super(cause);    }}

那么怎么确定继承哪个父类呢?

参考知乎大佬的回答:

继承Exception还是继承RuntimeException是由异常本身的特点决定的,而不是由是否是自定义的异常决定的。

例如我要写一个java api,这个api中会调用一个极其操蛋的远端服务,这个远端服务经常超时和不可用。所以我决定以抛出自定义异常的形式向所有调用这个api的开发人员周知这一操蛋的现实,让他们在调用这个api时务必考虑到远端服务不可用时应该执行的补偿逻辑(比如尝试调用另一个api)。此时自定义的异常类就应继承Exception,这样其他开发人员在调用这个api时就会收到编译器大大的红色报错:【你没处理这个异常!】,强迫他们处理。

又如,我要写另一个api,这个api会访问一个非常非常稳定的远端服务,除非有人把远端服务的机房炸了,否则这个服务不会出现不可用的情况。而且即便万一这种情况发生了,api的调用者除了记录和提示错误之外也没有别的事情好做。但出于某种不可描述的蛋疼原因,我还是决定要定义一个异常对象描述“机房被炸”这一情况,那么此时定义的异常类就应继承RuntimeException,因为我的api的调用者们没必要了解这一细微的细节,把这一异常交给统一的异常处理层去处理就好了。

作者:二大王
链接:https://www.zhihu.com/question/51970444/answer/128806764
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

总结:

  1. 如果一个方法很有可能出现错误,例如 Class.forName(“xxx”),如果全类名不存在,则方法出错。所以要将该方法出错时抛出的异常设置为受检异常,通知其他类,调用该方法时,注意避免错误的发生以及做好处理错误的准备。
  2. 如果一个方法不太可能出现错误,例如数组越界异常。不需要通知当前方法被调用时容易出现该异常。但异常也得被 catch ,不然会导致程序终止丢失异常信息后续无法排查。最好是将异常抛给全局异常处理层去处理及打印日志。
  • 8
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值