Java异常及处理方式

1.什么是异常?

1.1 异常的定义

在《Think in java》中是这样定义异常的:异常情形是指阻止当前方法或者作用域继续执行的问题。在这里一定要明确一点:异常代码某种程度的错误,尽管java是有异常处理机制,但是我们不能以“正常”的眼光去看待异常,异常处理机制的原因就是告诉你:这里可能会或者已经产生了错误,你的程序出现了不正常的情况,可能会导致程序失败!
那么什么时候才会出现异常呢?只有在你当前的环境下程序无法正常的运行下去,也就是说程序已经无法来正确解决问题了,这时它就会从当前环境中跳出,并抛出异常。
抛出异常时,虚拟机会做如下几件事:
1.使用new创建一个异常对象,然后 在产生异常的位置终止程序,并且从当前环境中弹出对异常对象的引用。
2.异常处理机制接管程序,并寻找一个恰当的位置来执行程序,这里的位置就是指异常处理程序。
3.记录异常信息,并反馈到控制台。

1.2 为什么要使用异常?

如果没有异常机制,那么通过函数的返回值去判断是否发生了异常,调用该函数的程序负责检查并且分析返回值,虽然可以解决异常,但是这样做存在几个缺陷:

1.容易混淆,如果返回值-1表示出现异常,那么当程序最后的计算结果真的是-1呢?

2.代码可读性差。将异常处理代码和程序代码混淆在一起将会降低代码的可读性。

3.由调用函数来分析异常,这要求程序员对库函数有很深的了解。

1.3 异常的分类

从这幅图中可以看出Throwable是所有异常的超类,拥有两个子类分别是Error与Exception,在实际开发中,普遍需要考虑的是Exception异常。

根据Java对异常的处理方式,我们大致可以将异常分为两类:

1.3.1 受检查的异常

受检查的异常(checked Exception):除了Error和RuntimeException的其他异常。Java中强制要求编程人员对这样的异常做预处理工作,例如try-catch或者throws,否则编译无法通过。这种异常大多与程序的运行环境有关,程序可能被运行在各种未知的环境下,而编程人员无法干预用户如何去使用他编写的程序,例如SQL EXception,IOException...等

1.3.2 非检查的异常

非检查异常(unchecked Exception):Error和RuntimeException以及他们的子类。Java在编译时,不会提示和发现这样的异常,不要求程序处理这些异常。对于这些异常,我们应该修正代码,因为这些异常的形成原因大多是代码编写有问题。

1.3.3 基本异常介绍

IOException :IO流引起的异常

EOFException:输入过程中,意外到达文件或流的末尾时,会抛出该异常。名字是End Of File的缩写,当然也表示流的末尾。

FileNotFoundException:读取文件时,发现文件路径不对或者根据对应路径无法找到对应文件,则会抛出该异常。在操作文件时,或多或少都会遇到这个异常,通常可能是文件路径错误引起的,我遇到的比较多就是因为相对路径/绝对路径用错了,当遇到这个异常的时候,需要先排查编写的路径是否可达。

MalformedURLException:当使用Java中的URL类时,如果传递给构造函数的URL格式不正确,则会抛出该异常。遇到该异常时,需要先检查对应的URL格式是否正确,可以使用正则表达式去验证。

UnknowHostException:在进行网络操作时,无法通过主机名找到对应的Ip地址,就会抛出该异常。

ClassCastException:类型转换错误时,会抛出该异常。通常是强制转换时,对应的类无法兼容,例如将String的对象强制转换成Integer对象。

ArithmeticExceptionException:在数学运算中发生了算术错误或不合法的操作就会抛出该异常。例如除数为0或者模数为0。

IIIegalArgumentException:当方法接收到一个非法或不合理的参数时,就会抛出该异常。通常用于参数校验。

NumberFormatException:在数字转换时,被转化对象格式不属于转化后的格式时,会抛出该异常,例如将一个"aaa"字符串转换为Integer。

IIIegalStateException:在不适当的时间或状态下调用方法时,会抛出该异常。例如在多线程环境下,多个线程对一个共享参数进行操作,且没有正确的同步方式。

ArrayIndexOutOfBoundsException:数组角标越界抛出的异常,一般在错误操作数组时会出现,例如数组长度为5,结果去取第六位的值。

InputMismatchException:输入的值数据类型与设置的值数据类型不能匹配,就会抛出该异常。NullPointerException:最最最常见的异常,就是操作空对象抛出的异常。例如去读取一个null对象的属性值。

2. 异常的使用

2.1 异常的出现

异常是在执行某个函数时引发的,而函数又是层级调用,形成调用栈的,因此,只要一个函数出现了异常,那么它所有的caller都会被异常影响,当这些函数会以异常信息输出时,就形成了异常堆栈。

 public static void main(String[] args) {
        int result = cal(1,0);
        System.out.println(result);
    }

    public static int cal(int num1,int num2){
        return num1/num2;
    }

运行结果:

从上面的例子可以看出,当cal函数发生除0异常时,会抛出对应的ArithmeticException异常,因此调用它的main函数也无法正常完成,并且将异常的堆栈打印在控制台中。这种行为叫做异常的冒泡,是为了当前发生异常的函数或者这个函数调用栈中找到最近的异常处理程序,由于上述例子中没有使用任何的异常处理机制,因此异常被main函数抛出,导致程序终止。

2.2 异常的处理方式

一般情况下,有两种处理方式:

try…catch…finally语句块包裹住可能会出现异常的代码。

使用throws 扔出异常。

public static void readFile() throws Exception {
        //在函数中抛出异常
        //这里的文件路径不存在
        FileInputStream stream 
            = new FileInputStream(new File("/user/test/file.txt"));
        int result ;
        while((result = stream.read()) != -1){
            System.out.println(result);
        }
        stream.close();
    }

上述代码会抛出FileNotFoundException,这是throws的基本用法。

接下来将代码优化,使用try…catch…finally的方式去处理异常

public static void readFile() {
        FileInputStream stream = null;
        try{
            //try块中放可能发生异常的代码。
            stream = new FileInputStream(new File("/user/test/file.txt"));
            int result ;
            while((result = stream.read()) != -1){
                System.out.println(result);
            }
        }catch (Exception e){
            //每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中可以将多个异常声明在一个catch中。
            //catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。
            //在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。
            //如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。
            //如果try中没有发生异常,则所有的catch块将被忽略。
            e.printStackTrace();
        }finally {
            //finally块通常是可选的。
            //如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。
            //无论异常是否发生,异常是否匹配被处理,finally都会执行。
            //finally主要做一些清理工作,如流的关闭,数据库连接的关闭等
            try {
                if(stream != null){
                    stream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

有些编程语言当异常被处理后,控制流会恢复到异常抛出点继续执行,这种策略叫做:恢复式异常处理模式。
而java则是让执行流恢复到处理了异常catch块后接着执行,这种策略叫做:终结式异常处理模式。

2.3 finally的使用

finally块不管异常是否发生,等对应的try中语句执行了,它也一定会执行,除非遇到了System.exit()。因此finally块通常被用来做资源释放操作:关闭文件,关闭数据库连接等等。
好的编程习惯是:在try块中打开资源,在finally中清理释放这些资源。
需要注意的是:
1.finally没有处理异常的能力,处理异常只能在catch块中
2.在同一个try...catch..finally中,如果try抛出异常,且有匹配的catch块,则会先执行catch在执行finally,如果没有catch匹配,则先执行finally,然后去外面的调用栈中寻找合适的catch块
3.在同一个try...catch..finally中,try发生异常,且匹配的catch处理异常时也抛出了异常,那么后面的finally也会执行:首先执行finally块,再去外面寻找合适的catch块。

2.3.1 当finally遇到了return

finally与return一起使用的场景在日常开发中也很常见,那么return后还会执行finally吗?finally与return的执行顺序是什么样的?

public static int finallyTest(){
        try{
            return 1;
        }finally {
            System.out.println("执行finally");
        }
        //输出 : 执行finally
    }

也就是说,try…catch…finally中的return 只要能执行,就都执行了,他们共同向同一个内存地址写入返回值,然后执行的将覆盖先执行的数据,而真正被调用者获取到的返回值
就是最后一次写入的,换句话说,return的执行在finally后。

public static int finallyTest(){
        try{
            return 1;
        }finally {
            System.out.println("执行finally");
            return 2;
        }
        //输出 : 执行finally
        //并且返回值为2
    }

使用finally时的建议:
1.不要在finally中使用return
2.不要在finally中抛出异常
3.减轻finally的任务,不要在finally中做一些其他的事情,仅仅用来释放资源是最合适的
4.尽量将所有的return写在函数的最后面,而不是try … catch … finally中。

3.异常的调用链

异常链化:以一个异常对象为参数构造新的异常对象,新的异常对象将包含先前异常的信息,这种技术主要是异常类的一个带Throwable参数的函数来实现,这个当做参数的异常,我们叫他根源异常(cause)
在一些大型的,模块化的软件开发中,一旦一个地方发生异常,则如多米诺骨牌一样,将导致一连串的异常。
假设B模块完成自己的任务需要调用A模块的方法,如果A模块发生异常,则B模块也无法正常结束,且发生异常。
但是B模块在抛出异常时,会将A的异常信息覆盖掉,这会导致异常的根源信息丢失。异常的链化就是将多个模块的异常串联起来。
使得异常信息不会丢失。
查看Throwable类源码,可以发现里面有个Throwable字段cause,就是它保存了构造时传递的根源异常参数。

public class Throwable implements Serializable {
    private Throwable cause = this;
    //...
    public Throwable(String message, Throwable cause) {
        fillInStackTrace();
        detailMessage = message;
        this.cause = cause;
    }
    //...
     public Throwable(Throwable cause) {
        fillInStackTrace();
        detailMessage = (cause==null ? null : cause.toString());
        this.cause = cause;
    }
    //........
}

4.自定义异常

如果要自定义异常类,则需要扩展Exception类即可,因此这样的自定义异常都属于检查异常,如果要自定义非检查异常,则需要扩张自RuntimeException
一般来说,自定义的异常应该总是包含如下的构造函数:
一个无参构造函数,一个带有String参数的构造函数,并传递给父类的构造函数。
一个带有String参数和Throwable参数,并都传递给父类构造函数。
一个带有Throwable参数的构造函数,并传递给父类的构造函数。
这里以IOException类的源码举例:

    public class IOException extends Exception
    {
        static final long serialVersionUID = 7818375828146090155L;
     
        public IOException()
        {
            super();
        }
     
        public IOException(String message)
        {
            super(message);
        }
     
        public IOException(String message, Throwable cause)
        {
            super(message, cause);
        }
     
        public IOException(Throwable cause)
        {
            super(cause);
        }
    }

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值