十、异常处理

异常机制可以使程序中的异常处理代码和正常业务代码分离,让代码更优雅,提高程序的健壮性。
而 Java 的异常机制主要依赖于 try、catch、finally、throw 和 throws 五个关键字。下面就来逐个击破!

异常概述

首先我们来考虑一个五子棋程序:当用户输入下棋的坐标时,程序要判断用户输入的是否合法,如下面伪代码:

if(用户输入包含除都好以外其它非数字字符)
{
    alert 坐标只能是数值
    goto retry
}
else if(用户输入不包含逗号)
{
    alert 应使用逗号分隔两个坐标值
    goto retry
}
else if(用户输入的坐标超出有效范围)
{
    alert 用户输入坐标应位于期盼坐标之内
    goto retry
}
else if(用户输入坐标已有棋子)
{
    alert 只能在没有棋子的地方下棋
    goto retry
}
else
{
    //业务实现代码
    ······
}

上面代码只是基础的一些处理,但实际上肯定要考虑的比这还要多。对于上面的处理机制,实在是代码太长了,而且很难想到所有的情况,那么现在我们需要一种机制来去处理,再看一个伪代码:

if(用户输入不合法)
{
    alert 输入不合法
    goto retry
}
else
{
    //业务实现代码
    ······
}

上面有一个很强的“if 块”——**程序不管输入错误的原因是什么,只要输入的不满足要求,程序就一次处理所有错误。**而这种机制就是异常处理。

使用 try …catch 捕获异常

接着上一个伪代码进行说明,用 try …catch 来写这个伪代码就是下面的样子:

try
{
    //业务实现代码
    ······
}
catch (Exception e)
{

    alert 输入不合法
    goto retry
}

如果业务实现代码可以顺利完成那么就“一切正常”,如果出现异常那么交给 catch 进行处理,这就是 try …catch 捕获异常。
需要大家知道的是,当 try 块里的业务逻辑代码出现异常,系统会自动生成一个异常对象,该异常对象被提交给 Java 运行时环境,这个过程被称为抛出(throw)异常。
当 Java 运行时环境收到异常对象时,会寻找能处理该异常对象的 catch 块(catch 块可以为多个,后面会介绍),如果没有 catch 块,则运行时环境终止,程序也将退出。

** try …catch 捕获异常要注意:**

  • 不管程序代码块在哪里,只要执行该代码块时出现了异常,系统总会自动生成一个异常对象。
  • try 块里声明的变量只在 try 内有效,在 catch 块不能访问该变量。

异常类的继承体系

在这里插入图片描述

上图是 Java 异常类的继承关系。
Java 把所有的非正常情况分为两个类:异常(Exception)和错误(Error)、它们都继承了父类 Throwable。

  • Error错误:一般指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可捕捉,导致应用程序中断,而大多数应用程序无法处理这些错误,所以我们不应该用 try…catch 来捕捉 Error 对象。
  • 而 Exception 异常:是指程序在运行时,发生了我们不想发生的事情,而这种情况大都可对程序进行改进和优化便可以处理。

下面来看一下为什么要给大家介绍异常类的继承关系?

    public static void main(String[] args) {
        Date d = null ;
        try
        {
            System.out.println(d.after(new Date()));
        }
        catch(NullPointerException ne)
        {
            System.out.println("空指针异常");
        }
        catch(Exception e)
        {
            System.out.println("未知异常");
        }
    }

上面程序调用一个 null 对象的方法,这会引发 NullPointerException 异常,此时 Java 运行时将会调用对应的 catch 块来处理异常,即第一个 catch 块;如果遇到其他异常, Java 运行时会调用最后一个 catch 块来处理异常。
那我们想一想,我们把这两个 catch 块进行位置调换一下会发生什么呢?

如果把 Exception 类对应的 catch 块排在第一个,Java 运行时会直接进行该 catch 块,而排在它后面的 catch 块将无法获得执行的机会(因为所有异常对象都是 Exception 的直接或间接子类)。
所以我们 catch 的顺序应遵循,先处理小异常,再处理大异常。

多异常捕获

从 Java 7 开始,一个 catch 块才能捕获多种异常;而在其之前,要想捕获多种异常,只能写多个 catch 块,每一个块处理一个异常。
多异常捕获需要注意两个地方:

  • 多种异常类型之间用竖线(|)隔开。
  • 异常变量有隐式的 final 修饰,因此程序不能对异常变量重新赋值。
    public static void main(String[] args) {
        try {
                int a = Integer.parseInt(args [0]);
                int b = Integer.parseInt(args [1]);
                int c = a / b;
            System.out.println("您输入的两个数相除的结果是" + c);
        }
        catch(IndexOutOfBoundsException|NumberFormatException|ArithmeticException ie)
        {
            System.out.println("程序发生了数组越界、数字格式异常、算术异常之一");
            //下面代码错误
            //捕捉多异常时,异常变量默认有 final 修饰,不能进行赋值处理
            ie = new ArithmeticException("test");
        }
        catch (Exception e)
        {
            System.out.println("未知异常");
            e = new ArithmeticException("test");
        }
    }

访问异常信息

如果我们想在 catch 块中访问异常对象的相关信息怎么办?
我们想一想之前的内容,系统发生异常时,总会生成一个异常对象,而当 Java 运行时决定调用某个 catch 块来处理该异常对象时,会将这个异常对象赋值给 catch 块后的异常参数,我们可以通过这个参数来或得异常的相关信息。下面是异常对象包含的几个常用方法:

  • getMessage():返回该异常的详细描述字符串。
  • printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。
  • printStachTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。
  • getStackTrace():返回该异常的跟踪栈信息。

(跟踪栈在这些方法中出现了很多次,先不用着急,后面会进行讲解。)
我们来试一下这些方法:

   public static void main(String[] args) {
        try {
            FileInputStream fis = new FileInputStream("a.txt");
        }
        catch (IOException ioe)
        {
            System.out.println(ioe.getMessage());
            ioe.printStackTrace();
        }
    }

下面是运行结果(使用的是有关I/O流的方法,先不用知道具体方法是干什么用的,我们这里的重点是异常哦~)
在这里插入图片描述

使用 finally 回收资源

有时候,程序在 try 块里打开一些物理资源(如网络连接、磁盘文件等),而这些物理资源都需要显式回收,**那在哪里进行显式回收呢?try 块?如果 try 某条语句引起了异常,则该语句后其他语句通常不会活动执行的机会了。catch 块?如果没有异常,catch 块可能没有机会得到执行。**这个时候该 finally 出马了!

Java 的垃圾回收机制不会回收任何物理资源,只能回收堆内存中对象所占用的内存。

来看一下使用 finally 的语法结构是什么样的:

 public static void main(String[] args) {
        try {
            //业务实现代码
        }
        catch (Subxception1 e)
        {
            //异常处理块1
        }
        catch (Subxception2 e)
        {
            //异常处理块2
        }
        finally {
            //资源回收
        }
    }
**异常处理语法结构中只有 try 块是必需的,而 catch 块和 finally 块都是可选的,但二者至少出现一个。若同时出现,finally 块必需位于所有 catch 块之后。**

**通常情况下,不要在 finally 块中使用 return 或 throw 等可以让方法终止的语句。因为这样会让 try 块、catch 块中的 return 或 throw 语句失效。**看如下程序:

        public static void main(String[] args) {
            boolean a = test();
            System.out.println(a);
        }
        public static boolean test()
        {
            try {
                return true;
            }
            finally {
                return false;
            }
        }
//res:
false
结果如我们所料,那我们来梳理一下流程:

当程序执行 try 块、catch 块 遇到了return 或 throw 语句,这两个语句都是结束方法,但是系统不会就此结束,而是去寻找这个程序里是否有 finally 块,如果没有的话,那程序就结束了;如果有的话,那程序就执行 finally 块内的代码,并且执行完之后,才会跳回去执行 try 块、catch 块内的 return 或 throw 语句;如果 finally 块里有终止语句,系统就跳不回去了,而是就此结束。

Check 异常和 Runtime 异常体系

Java 的异常被范围两大类:Check 异常和 Runtime 异常,所有 RuntimeException 异常及其子类的实例被称为 Runtime 异常;其它的异常则被称为 Checked 异常。
只有 Java 语言提供了Checked 异常,Java 认为 Checked 异常都是可被修复的异常。如果没有处理这些异常,该程序在编译时就会发生错误。对 Checked 异常一般处理方式如下:

  • 明确知道如何处理该异常,则应该使用 try…catch 来捕获异常,并在对应的 catch 块中修复该异常。
  • 不知道如何处理这种异常,应该在定义该方法时声明抛出该异常。

而 Runtime 异常无须显式声明抛出,也可进行捕捉处理,十分灵活。

使用 throws声明抛出异常

使用 throws 的思路是:当前方法不知道如何处理这种异常,丢给上一级调用者处理。
**throws 声明只能在方法签名中使用,并且可以抛出多个异常类。**使用如下:

        public static void main(String[] args)
            throws IOException, SQLDataException
        {

        }

一旦使用 throws 抛出异常后,程序就无须使用 try…catch 块来捕捉异常了。
使用 throws 声明抛出异常有一个限制子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。

使用 throw 抛出异常

throw 和 throws 是有很大区别的哦~
Java 允许程序自行抛出异常,自行抛出异常时通过 throw 语句完成。**throw 可以单独使用,并且 throw 抛出的不是异常类,而是一个异常实例,每次只能抛出一个异常实例。**继续来拿五子棋来说事:

           try {
                if(下棋的地方已经有棋子了){
                    throw new Exception("您下棋的地方已经有棋子了");
                }
            }
            catch (Exception e){
                System.out.println("您输入的坐标不合法,请重新输入");
                continue;
            }
        }
if 判断我为了方便易理解,拿汉字代替了,

自定义异常类

通常情况下,程序很少会自行抛出系统异常,在这种情况下,应用程序常需要抛出自定义异常。**用户自定义异常应该继承 Exception 基类,如果自定义 Runtime 异常,应该继承 RuntimeException 基类。而定义异常类需要提供两个构造器:一个是无参数构造器;另一个是带一个字符串参数的构造器,这个字符串作为该异常对象的描述信息。**下面我们来自定义一个异常类:

       public  class AuctionException extends Exception
        {
            //无参构造器
            public AuctionException(){}
            //带一个字符串参数的构造器
            public AuctionException(String msg)
            {
                super(msg);
            }
        }
如果要自定义** Runtime 异常,将上面的 Exception,改成 RuntimeException 即可。**

注:自定义的异常类的类名要可以准确描述该异常。

catch 和 throw 同时使用

前面介绍了两个异常处理的方法:

  • 在出现异常的方法内捕捉并处理异常,直接在 catch 块中进行处理,而调用者将不能再次捕捉异常。
  • 在该方法的签名中声明抛出该异常,将异常完全交给方法调用者去处理。

实际情况中的情况,可能单靠某一个方法无法完全处理该异常,也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有一些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者也能捕捉到异常。
为了处理这种情况,可以在 catch 块中结合 throw 语句来完成,下面用五子棋进行示范:

           try {
                if(下棋的地方已经有棋子了){
                    throw new Exception("您下棋的地方已经有棋子了");
                }
            }
            catch (Exception e){
                //在控制台打印异常的跟踪栈信息
                e.printStackTrace();
                //抛出自定义异常
                throw new AuctionException("您输入的坐标不合法");
            }
        }

Java 的异常跟踪栈

来把之前的坑给填一下,什么是跟踪栈?
之前说过,异常对象的 printStackTrace() 方法用于打印异常的跟踪栈信息,根据该方法的输出结果,开发者可找到异常的源头,并跟踪到异常一路触发的过程。

class test{
        public static void main(String[] args)
        {
            firstMethod();
        }
    public static void firstMethod()
    {
        secondMethod();
    }
    public static void secondMethod()
    {
        thirdMethod();
    }
    public static void thirdMethod()
    {
        throw new SelfException("自定义异常信息");
    }
}
        //自定义异常
          class SelfException extends RuntimeException
        {
            //无参构造器
            SelfException(){}
            //带一个字符串参数的构造器
            SelfException(String msg)
            {
                super(msg);
            }
        }

上面的代码也很好理解,firstMethod 调用 secondMethod, secondMethod 调用 thirdMethod,thirdMethod 直接抛出自定义异常。
下面是打印的结果:
在这里插入图片描述

上图可得,异常从 thirdMethod 方法开始触发,传到 secondMethod 方法,再传到 firstMethod 方法,最后传到 main 方法,在 main 方法终止,这个过程就是 Java 的异常跟踪栈。

注意: 虽然 printStackTrace() 方法可以很方便追踪异常发生的情况,可以用来调试程序,但在最后发布程序中,应该避免使用;取而代之的一个是对捕捉异常并进行处理!

异常处理规则

  • 不要过度使用
    • 过度使用可能会把异常和普通代码混淆一起,不在编写任何错误处理代码,而是简单抛出敷衍了事。
    • 过度使用可能会导致使用异常来处理来代替流程控制,这显然是不可取的。
  • 不要使用过于庞大的 try 块
    • 会造成 try 块中出现异常的可能性大大增加,并且导致分析异常原因的难度也大大增加。
    • 难免会在 try 块后紧跟大量 catch 块,才能针对不同的异常提供不同的处理逻辑,这反而会增加编程的难度。
  • 不要忽略捕获到的异常
    • 既然捕捉到了异常,那 catch 块就应该理所应当的做一些有用的事情,并且 catch 块不能为空!这样会导致程序出现错误,但所有人都看不到,这是非常严重的事情!
    • 也可以选择重新抛出异常,把异常包装成当前层的异常,抛出给上层调用者
    • 在合适的层处理异常。

常见的异常

1、java.lang.ArithmeticException
算术运算异常,因为除数为0,所以引发了算数异常
2、java.lang.StringIndexOutOfBoundsException: String index out of range: -1
这是截取字符串substring()产生的下标越界异常。原因是可能是字符串为空,或长度不足1
3、java.lang.NullPointerException空指针异常
出现该异常的原因在于某个引用为null,但却调用了它的某个方法,这时就会出现该异常
4、ClassCastException
类型强制转换异常,例如:Object x = new Integer(0);System.out.println((String)x);
5、IllegalArgumentException
传递非法参数异常,此异常表明向方法传递了一个不合法或不正确的参数。你看看传值的方法是否参数不正确
6、NumberFormatException
数字格式异常,例如:把"176//240"这个输入字条转换为一个数字
7、ClientAbortException: java.io.IOException异常
原因是由于服务器正在处理http请求,正在输出内容时,用户关闭了浏览器,造成了ClientAbortException异常。它属于I/O异常中比较常见的一种。
**8、ClientAbortException **
这种异常已比较常见,通常有以下几种情况:
服务器的并发连接数超过了其承载量,服务器会将其中一些连接Down掉;客户关掉了浏览器,而服务器还在给客户端发送数据
9、ArrayStoreException
向数组中存放与声明类型不兼容对象异常,例如:Object x[] = new String[3];x[0] = new Integer(0);
10、NegativeArraySizeException
创建一个大小为负数的数组错误异常,例如int[] arr = new int[10];int i = arr[-1];
11、SecurityException
安全异常,例如:android的权限异常,运行java的程序提示Missing requited Permissions manifest attribute in main jar等
12、UnsupportedOperationException
不支持的操作异常,例如String testStr = “100,300,400,545,666”;List test = Arrays.asList(testStr.split(","));test.remove(“100”);使用Arrays.asList()方法一定要注意类型的转换。

(常见异常来源:https://blog.csdn.net/qq_17168031/article/details/51433849
(参考资料:疯狂 Java 讲义)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值