Java 基础 - 异常处理

2021/4/25 1:25:18

本文主要是介绍Java 基础 - 异常处理,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

异常体系

Error

一般为底层的不可恢复的类,一般此类错误都比较严重,JVM将终止其运行的线程;

  • VirtualMachineError:虚拟机运行错误;
  • OutOfMemoryError:内存溢出;

Exception

程序本身可以捕获并且可以预处理的异常,例如捕获或者抛出;

  • CheckException
    受检查异常,编译阶段必须处理;
    编写异常类时直接继承Exception让它成为一个受检异常

  • RuntimeException
    运行时异常,可不用捕获,其实Exception都是受检异常,RuntimeException反而可以看成一种特例
    运行时异常场景

    1. 个人理解:不希望每个方法都处理异常,而是由最上层统一处理

异常声明

throws允许你对某个受检异常不进行处理,而是抛出去交给上层调用方处理

  • 受检异常
    受检异常只要抛出去了,必须声明throws,调用方也必须要处理(或者继续抛出)
  • 运行时异常
    运行时异常可以抛出时可以不用声明,声明了调用方也不一定要捕获

栈轨迹

错误的栈轨迹,栈顶元素是抛出异常的地方

public class ExceptionStack {
    public static void main(String[] args) {
        h();  
    }

    private static void f() {
        try{
            throw new RuntimeException();
        } catch (Exception e) {
            for (StackTraceElement element : e.getStackTrace()) {
                System.out.println(element.getMethodName());
            }
        }
    }

    private static void g() {
        f();
    }

    private static void h() {
        g();
    }
}

测试结果

f
g
h
main

重新抛出原有异常

catch(Exception e) {
    throw e;
}

堆栈信息不会记录新的调用点,重新抛出的地方不会成为栈顶元素
即如果直接抛出原来的异常对象,错误的堆栈信息不会改变

public class ExceptionStack2 {
    public static void main(String[] args) {
        h();
    }

    private static void f() {
        throw new RuntimeException();
    }

    private static void g() {
        try {
            f();
        } catch (Exception e) {
            System.out.println("g() : 重新抛出异常");
            throw e;
        }
    }

    private static void h() {
        try {
            g();
        } catch (Exception e) {
            for (StackTraceElement element : e.getStackTrace()) {
                System.out.println(element.getMethodName());
            }
        }
    }
}

测试结果

g() : 重新抛出异常
f
g
h
main

重新构建异常

如果重新构建新的异常,那么栈顶元素就是抛出的地方了,就跟正常抛出一样

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

    private static void f() {
        throw new RuntimeException();
    }

    private static void g() {
        try {
            f();
        } catch (Exception e) {
            System.out.println("g() : 重新构建异常抛出");
            throw new RuntimeException("g() : exception");
        }
    }

    private static void h() {
        try {
            g();
        } catch (Exception e) {
            for (StackTraceElement element : e.getStackTrace()) {
                System.out.println(element.getMethodName());
            }
        }
    }

测试结果
从测试结果可以看出,g()这个方法抛出的异常不会保留f()方法调用它的轨迹了

g() : 重新构建异常抛出
g
h
main

异常链

上面提到重新构建异常抛出时无法得到完整的调用链了,那么我们如何得到上一个异常发生的信息呢?

在我们重新抛出异常的时候希望保存原来的异常对象信息,JDK1.4之后我们就可以在构造时传入一个cause异常对象,保留上一个异常对象的信息

在堆栈打印时会通过 Cause by 打印上一个异常对象的栈轨迹

public static void main(String[] args) {
        try {
            h();
        } catch (Exception e){
            e.printStackTrace();
        }
    }

    private static void f() {
        throw new RuntimeException("f(): exception");
    }

    private static void g() {
        try {
            f();
        } catch (Exception e) {
            throw new RuntimeException("g(): exception",e);
        }
    }

    private static void h() {
        try {
            g();
        } catch (Exception e) {
            throw new RuntimeException("h(): exception",e);
        }
    }

测试结果

java.lang.RuntimeException: h(): exception
	at example.exception.usage.demo2.ExceptionChain.h(ExceptionChain.java:28)
	at example.exception.usage.demo2.ExceptionChain.main(ExceptionChain.java:6)
Caused by: java.lang.RuntimeException: g(): exception
	at example.exception.usage.demo2.ExceptionChain.g(ExceptionChain.java:20)
	at example.exception.usage.demo2.ExceptionChain.h(ExceptionChain.java:26)
	... 1 more
Caused by: java.lang.RuntimeException: f(): exception
	at example.exception.usage.demo2.ExceptionChain.f(ExceptionChain.java:13)
	at example.exception.usage.demo2.ExceptionChain.g(ExceptionChain.java:18)
	... 2 more

initCause

某些异常的构造函数中可能没有cause(Throwable)这种参数(例如FileNotFoundException),但是我们还可以用initCause来保存上一个异常对象
这里仅作演示,RuntimeException是可以直接通过构造的方式保存异常链的

 private static void g() throws Exception {
        try {
            f();
        } catch (Exception e) {
            Exception exception = new RuntimeException("g(): exception");
            exception.initCause(e);
            throw exception;
        }
    }

finally

finallytry/try-catch后一定会执行的语句

return 与 finally

  1. 就算try return了, finally也会执行
public static void main(String[] args) {
        System.out.println("get(): " + get());
    }

    private static int get() {
        try {
            return 1;
        } finally {
            System.out.println("就算try return了, finally也会执行");
        }
    }

测试结果

就算try return了, finally也会执行
get(): 2
  1. finally里面return
    会使用finallyreturn,所以最好别这么做
public static void main(String[] args) {
        System.out.println("get(): " + retInfinally());
    }

    private static int retInfinally() {
        try {
            return 1;
        } finally {
            return 2;
        }
    }

测试结果

get(): 2

finally丢失异常

  1. finally中抛出的异常会覆盖trycatch中抛出的异常
    你也许不会直接在finally中抛出异常,但你有可能在finally中调用其他方法,如果抛出了调用方法时抛出了异常,则会覆盖原有异常
public static void main(String[] args) {
        System.out.println("throwInfinally(): " + throwInfinally());
    }

    static class ImportantException extends Exception {
    }

    private static int throwInfinally() {
        try {
            throw new ImportantException();
        } finally {
            throw new RuntimeException("finally中抛出:运行时异常");
        }
    }

运行结果

Exception in thread "main" java.lang.RuntimeException: finally:运行时异常
	at example.exception.usage.demo3.FinallyTest2.throwInfinally(FinallyTest2.java:25)
	at example.exception.usage.demo3.FinallyTest2.main(FinallyTest2.java:7)

  1. finally中进行return也会吞掉异常
    你可以发现即使我们抛出了一个受检异常也不会编译报错,因为我们在finally中return了
    public static void main(String[] args) {
         System.out.println("get(): " + retInfinally());
     }
     
     static class ImportantException extends Exception {
     }
    
     private static int retInfinally() {
         try {
             throw new ImportantException();
         } finally {
             return 2;
         }
     }
    

运行结果

get(): 2

资源清理与构造器

finally常被用来做资源清理,但是资源清理的前提是资源确实被初始化了,那如果资源初始化就失败了呢?那么finally中就不应该进行资源清理,比较好的方式是通过嵌套try语句,这里就直接用《OnJava8》中的demo了

public class Cleanup {
    public static void main(String[] args) {
        try {
            InputFile in = new InputFile("Cleanup.java");
            try {
                String s;
                int i = 1;
                while((s = in.getLine()) != null)
                    ; // Perform line-by-line processing here...
            } catch(Exception e) {
                System.out.println("Caught Exception in main");
                e.printStackTrace(System.out);
            } finally {
                in.dispose();
            }
        } catch(Exception e) {
            System.out.println(
                    "InputFile construction failed");
        }
    }
}

try-with-resources

概念

从上面的例子中我们可以发现资源的初始化与释放操作手动编写比较复杂,所以在从Java7开始提供了 try-with-resources

基本原理就是通过实现AutoCloseable接口,完成自己的资源释放操作,所以只要是实现了这个接口的类我们都可以通过try-with-resources在try语句中进行资源的初始化

基本用法

这里还是直接使用《OnJava8》中的例子:

 public static void main(String[] args) {
        try(
                InputStream in = new FileInputStream(
                        new File("TryWithResources.java"))
        ) {
            int contents = in.read();
            // Process contents
        } catch(IOException e) {
            // Handle the error
        }
    }

资源声明顺序

需要构建多个资源对象时可以用分号分割,注意顺序,后声明资源的会先关闭

异常与继承

这个就是我们常常背的重写要求之一了,即重写的方法不能抛出比父类范围更广的异常
这样做的意义就是保证接口的通用性:

  1. 子类可以不抛出异常,但是调用方针对基类处理了异常那也没问题
  2. 子类抛出基类的异常或者范围更小的异常,调用方针对基类做的异常处理仍然生效

异常匹配

被第一个匹配的catch捕获,例如说你不能先catch一个Exception,再catch它的子类,编译器会报错,因为后面的catch语句永远匹配不到

try {
            // do something
        } catch (Exception e){
            e.printStackTrace();
        } catch (RuntimeException e){  // 这样是不行滴

        }

异常使用指南

这里直接贴一下《OnJava8》中的异常指南

应该在下列情况下使用异常:

  1. 尽可能使用 try-with-resource。
  2. 在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常。)
  3. 解决问题并且重新调用产生异常的方法。
  4. 进行少许修补,然后绕过异常发生的地方继续执行。
  5. 用别的数据进行计算,以代替方法预计会返回的值。
  6. 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
  7. 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
  8. 终止程序。
  9. 进行简化。(如果你的异常模式使问题变得太复杂,那用起来会非常痛苦也很烦人。)
  10. 让类库和程序更安全。(这既是在为调试做短期投资,也是在为程序的健壮性做长期投资。

------ 《OnJava8》

异常日志

异常日志一版记录哪些信息?

  1. 异常类型
  2. 异常信息
  3. 业务参数
  4. 异常时间
  5. 异常位置

首先就是看下异常时间,即什么时候发生的异常,然后通过异常类型可以判断是哪一块有问题,通过异常信息和业务参数进一步确定异常,如果还解决不了,那就得通过异常位置看代码

最后

关于异常处理其实还有很多内容值得学习和研究,例如:
Java中处理异常的一些场景,什么时候应该抛出原有异常,什么时候应该重新构建新的异常?
其他语言是怎么处理异常的?

极客上有几篇专栏是与异常处理相关的,等看完那几篇文章研究一下之后再来进行记录

异常处理:别让自己在出问题的时候变为瞎子

程序中的错误处理:错误返回码和异常捕捉



这篇关于Java 基础 - 异常处理的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程