Java基础深度总结:异常

君志所向,一往无前,愈挫愈勇,再接再厉。

1.异常概述

异常指不期而至的各种状况,如:文件找不到、网络连接失败、除0操作、非法参数等。异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程。为了应对运行期间可能出现的错误,提高程序的的稳健性,Java中定义了强大的异常处理机制,使用异常处理机制可以降低错误处理代码的复杂度:

  • 异常处理程序中集中处理错误,节省代码,不必每次都去处理。
  • 执行过程和异常处理相分离,更易阅读、编写、调试。
2.Java的异常处理机制

Java把异常当作对象来处理,并通过抛出异常和捕获异常完成对各种异常的处理。

抛出异常:抛出异常是指:当前环境下无法获得必要的信息来解决问题,就从当前环境中跳出,并把问题提交给上一级环境。抛出异常后,首先,将使用new在堆上创建一个异常对象,然后当前的执行路径被终止,并且从当前环境中弹出对异常对象的引用。

捕获异常:在方法抛出异常之后,异常处理器通过捕获异常的类型,来执行相应异常类型的异常处理程序或者异常处理器,让程序重新恢复到稳定状态。

3.Java异常体系和分类

在这里插入图片描述
上面这张图展示了Java异常的继承结构关系,可以发现所有的异常都是Throwable的子类。

Java语言提供了三种可抛结构:受检异常、运行时异常、错误,其中点运行时异常和错误统称非受检异常。

  • 受检异常(又称非运行时异常)编译器要求必须处理的异常,要么处理异常,要么在 异常说明中表明此方法将产生异常,否则编译不通过。除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常,如:IOException、SQLException、ClassNotFoundException(异常说明稍后会在throws中详细讲到)
  • 运行时异常编译器不要求强制处置的异常。包括RuntimeException及其子类,他们会被JVM自动抛出、自动捕获,一般由程序员粗心大意造成,是编程错误,这些错误在编码过程中是可以避免的,如:NullPointerException(空指针异常)、ArrayIndexOutOfBoundsException(数组下标越界异常)。.
  • 错误(Error):指程序无法处理的错误,编译器不要求强制处置。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM 出现的问题,如当虚拟机内存溢出时出现:OutOfMemoryError,这些异常发生时,JVM一般会选择线程终止。
4.throw和throws:异常抛出和异常声明

throw:
throw总是出现在方法体中,用来抛出一个Throwable类型的异常对象。例如:

if(obj == null){
	throw new NullPointerException();
}

使用throw应该注意:

  • throw必须抛出Throwable的子类对象。
  • 程序执行完throw语句会从当前作用域退出,其后的语句不再执行。
  • 不要在构造函数中抛出异常,这可能会导致对象创建失败。

throws:
throws用于方法的异常说明,它属于方法声明的一部分,紧跟形参列表之后,列举了一个方法可能引发的所有异常类型。例如:

void f() throws IllegalAccessException, IOException{
	//....
}

当一个方法中通过throw关键字抛出了多个异常,而又不想或者没有能力去处理这些异常时,可以通过throws将这些异常抛给调用者。

使用throws应该注意:

  • 如果是非受检异常,即Error、RuntimeException或它们的子类,那么可以不使用throws关键字来声明要抛出的异常,当发生非受检异常时,JVM自动抛出该异常。
  • 如果一个方法可能出现受检异常,要么用try-catch语句捕获处理,要么用throws子句声明将它抛出。
  • 如果方法中存在多个未被处理的受检异常,那么这些受检异常必须在throws后全部声明。
  • 子类重写父类方法时,被重写的方法可以抛出任意非受检异常,不能抛出新的受检异常,子类抛出的受检异常必须是被重写方法的本类或子类。
class TestThrows {

    static void throw1() { //RuntimeException是非受检异常,可以在throws后省略
        throw new RuntimeException();
    }
	
	//IllegalAccessException和IOException 均为受检异常,且未被处理
    static void throw2() throws IllegalAccessException, IOException {

        boolean flag = new Random().nextBoolean();
      
        if (flag) {
            throw new IllegalAccessException();
        } else {
            throw new IOException();
        }
    }
}

class Fu {
    void f() throws IOException {
    }
}

class Zi extends Fu {
    //子类重写的方法可以抛出任意非受检异常如:RuntimeException、Error
    //子类重写的方法抛出的受检异常必须是父类受检异常的本类或子类,	
    //FileNotFoundException为IOException的子类
    @Override
    void f() throws FileNotFoundException, Error{
    }
}

Tips:

  • 可以声明方法抛出一个异常而实际不抛异常,编译器会强制用户像真的抛出异常来使用这个方法,这样以后就可以抛出这种异常而不用修改已有的代码。
  • throw和throws相差甚远,几乎没什么关系。throw只出现在方法体中,用于抛出异常;throws出现在方法声明上,用于声明方法可能出现的异常。
5.try-catch-finally异常处理
5.1 语法形式

在Java中,异常通过try-catch-finally语句捕获处理。其一般语法形式为:

try {  
	// 可能会发生异常的程序代码  
} catch (Type1 id1){  
	// 捕获并处置try抛出的异常类型Type1  
} catch (Type2 id2){  
	 //捕获并处置try抛出的异常类型Type2  
}finally{
	// 无论是否发生异常,都会执行finally中的语句
}

try块:try包含一块可能发生异常的代码,称为监控区域,用于捕获区域内出现的异常。

catch块:用于匹配和处理捕获到的异常。

finally块:无论是否发生异常,都会执行finally中的语句,通常用于释放资源,如断开网络连接、关闭文件等。

5.2 规则
  • try块后可接多个catch语句块,finally为可选语句,可以与catch语句块同时存在,如果没有catch语句块,则try块后必须跟一个finally语句块。
  • 必须遵循块顺序:try --> catch --> finally 。
  • 若try捕获的异常类型与catch声明的异常类型一致,或try捕获的异常类型是catch声明的异常类型的子类,则匹配成功,然后执行匹配成功的catch块。
  • 多个catch块按书写顺序匹配,catch中子类异常类型必须在父类异常类型之前,否则编译不通过。
  • 一旦某个catch捕获到匹配的异常类型,将进入异常处理代码。一经处理结束,就意味着整个try-catch语句结束,其他的catch子句不再有匹配和捕获异常类型的机会。
  • finally语句不是一定会执行,例如:JVM过早终止(调用了System.exit(0);)、finally语句块有未被处理的异常、突然断电等都可能导致finally语句没有被执行。
5.3 执行顺序

在这里插入图片描述

  • 当try没有捕获到异常时:try语句块中的语句逐一被执行,程序将跳过catch语句块,执行finally语句块和其后的语句。
  • 当try捕获到异常,在try语句块中当执行到某一条语句出现异常时,程序将跳到catch语句块,并与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行,而try语句块中,出现异常之后的语句也不会被执行,catch语句块执行完后,执行finally语句块里的语句,最后执行finally语句块后的语句。
  • 当try块或catch块中有return语句、或者catch块中有throw语句时,会在程序返回或抛出异常之前执行finally语句块。

Tip:
如果在try块抛出的是受检异常,那么一定会被catch某块匹配成功,try块抛出异常都是非受检异常,如果catch匹配失败将会把异常抛给方法调用者。

5.4 不要在finally块中做与资源释放无关的操作

finally语句块设计的初衷是用来释放资源的,不管是否发生异常,比如断开网络连接,关闭文件等,在finally块中其他操作可能使得程序出现奇怪的问题。下面列举了两个情况:

(1)finally中修改数据

public class FinallyUpdateTest{

    static Dog f1() {

        Dog dog = new Dog("旺财", 3);

        try {
            throw new IllegalAccessException();
        } catch (Exception e) {
            System.out.println("f1:catch");
            return dog;
        } finally {
            System.out.println("f1:finally");
            dog.setName("阿旺");
        }
    }

    static int f2() {

        int num = 10;

        try {
            throw new IllegalAccessException();
        } catch (Exception e) {
            System.out.println("f2:catch");
            return num;
        } finally {
            System.out.println("f2:finally");
            num = 20;
        }
    }

    public static void main(String[] args) {
        System.out.println(f1());
        System.out.println(f2());
    }
}

class Dog {

    private String name;
    private int age;

    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void setName(String name) {
        this.name = name;
    }


    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

输出结果:
f1:catch
f1:finally
Dog{name='阿旺', age=3}
f2:catch
f2:finally
10

解析:
在执行finally语句块的内容之前,return的值会先被压入栈中,执行完finally语句块内容后,才会将return的值从栈中取出,最后返回。因此对于基本数据类型和String类型,finally的修改操作不会影响到原来的值,但对于引用类型来说,finally修改的是该引用所指向的对象的值。

(2)finally中的return

public class FinallyReturnTest {

    static int f1() {
        try {
            int b = 2 / 0;
            return 0;
        } catch (Exception e) {
            System.out.println("f1:catch");
            return 1;
        } finally {
            System.out.println("f1:finally");
            return 2;
        }
    }

    static void f2() throws Exception {
        try {
            int b = 2 / 0;
        } catch (Exception e) {
            System.out.println("f2:catch");
            throw e;
        } finally {
            System.out.println("f2:finally");
            return;
        }
    }

    public static void main(String[] args) {

        System.out.println("f1: return " + f1());

        try {
            f2();
        } catch (Exception e) {
            System.out.println("f2()抛出异常");
        }
    }
}

输出:
f1:catch
f1:finally
f1: return 2
f2:catch
f2:finally

从输出结果可以看出来:

  • finally中的return让方法提前返回,屏蔽了原来的返回值,同时也导致catch 抛出的异常丢失。
6.栈轨迹

printStackTrace()方法可以打印一个由栈轨迹中的元素构成的数组,数组中的每一个元素都是一个栈帧。数组的第一个元素保存的是栈顶元素,最后一个元素保存的栈底元素。

public class StackTraceTest {

    static void f1() throws Exception{
        f2();
    }

    static void f2() throws Exception{
        f3();
    }

    static void f3() throws Exception{
        throw new Exception();
    }

    public static void main(String[] args) {
        try {
            f1();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
输出:
java.lang.Exception
	at StackTraceTest.f3(StackTraceTest.java:14)
	at StackTraceTest.f2(StackTraceTest.java:10)
	at StackTraceTest.f1(StackTraceTest.java:6)
	at StackTraceTest.main(StackTraceTest.java:19)

f3 --> f2 --> f1 --> main

7.异常链

有时候我们会捕获一个异常后在抛出另一个异常,并希望保留原始信息,称为异常链。JDK1.4后Throwable中所有子类构造函数都可以传递一个Throwable子类作为因由(Cause)参数来保存原始的异常信息。这样就算抛出了新的异常,也能通过该异常追踪到异常最初发生的位置。

public class ExceptionLink {

    static void f1() throws Exception{
        try {
            f2();
        } catch (Exception e) {
            throw new Exception(e);
        }
    }

    static void f2() throws Exception{
        try {
            f3();
        } catch (Exception e) {
            throw new Exception(e);
        }
    }

    static void f3() throws Exception{
        throw new Exception();
    }

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

}

输出:
java.lang.Exception: java.lang.Exception: java.lang.Exception
	at ExceptionLink.f1(ExceptionLink.java:9)
	at ExceptionLink.main(ExceptionLink.java:27)
Caused by: java.lang.Exception: java.lang.Exception
	at ExceptionLink.f2(ExceptionLink.java:17)
	at ExceptionLink.f1(ExceptionLink.java:7)
	... 1 more
Caused by: java.lang.Exception
	at ExceptionLink.f3(ExceptionLink.java:22)
	at ExceptionLink.f2(ExceptionLink.java:15)
	... 2 more

从中可以清楚的看到f1调用失败的因由f2,以及f2调用失败的因由f3。

8.自定义异常类

自定义异常类必须从已有的异常类继承,因此实现一个自定义异常类就非常容易了。

public class FormatException extends Exception
{
    public FormatException(String message) {
        super(message);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值