java中的异常处理机制

异常概述
异常处理已经成为衡量一门语言是否标准的标准之一。增加了异常处理机制后的程序有更好的容错性。


异常处理机制

java的异常处理机制可以让程序员具有极好的容错性,让程序更加健壮。当程序运行出现意外情形时,系统会自动生成一个Expection对象来通知程序,从而实现将“业务功能实现代码”和“错误处理代码”分离,提供更好的可读性。

使用try…catch捕获异常
语法结构

try{
	//业务实现代码
	...
}
catch(Exception e){
	alert输入不合法
}

当try块里的业务逻辑代码出现异常时,系统会自动生成一个异常对象,该异常对象被提交给java运行时环境,这个过程被称为抛出异常
当运行时环境收到异常对象时,会寻找能处理该异常对象的catch块,如果找到合适的catch块,则把该异常对象交给该catch快处理,这个过程被称为捕获异常;如果java运行时环境找不到捕获异常的catch块,则运行时环境终止,java程序也将退出。

运行下面的程序,报出异常ArithmeticException: / by zero,除数不能为0

    public static void main(String[] args) {
        int a=1;
        int b=0;
        System.out.println(1/0);
        //Exception in thread "main" java.lang.ArithmeticException: / by zero
    }

用try…catch捕获这个异常,友好的处理。

    public static void main(String[] args) {
        int a=1;
        int b=0;
        try {
            System.out.println(1/0);
        }catch (ArithmeticException e){
            System.out.println("除数不能为0");
            //除数不能为0
        }
    }

异常类的继承体系

当java运行时环境收到异常后,会依次判断该异常对象是否是catch块后异常类或其子类的实例,如果是,java运行时环境将调用该catch块来处理异常;否则再次拿该异常对象和下一个catch块里的异常类进行比较。

java中的异常处理机制_第1张图片

当程序进入负责异常处理的catch块时,系统生成的异常对象ex将会传给catch后的异常参数。try后面可以跟catch块,当系统发生不同的异常情况时,系统会生成不同的ex对象。在通常情况下如果try块被执行一次,则try后只有一个catch块被执行,除非使用continue,否则不会执行多个catch块。

java中异常类的继承

java把所有非正常情况分为两种:异常(Exception)和错误(Error),都集成Throwable父类。
java中的异常处理机制_第2张图片

Error错误,一般是指与虚拟机相关的问题,这种错误无法恢复或不可能捕获,将导致应用程序中断。
处理异常规则:先处理小异常,再处理大异常

java7提供的多异常捕获
从java7开始一个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){
            //异常变量默认有final修饰
            System.out.println("程序发生了数组越界、数字格式异常、算数异常之一");
        }catch (Exception e){
            System.out.println("未知异常");
        }
    }

访问异常信息

如果程序需要在catch块中访问异常对象的相关信息,则可以通过访问catch块后的异常形参来获得。当决定调用某个catch块来处理该异常时,会将异常对象赋值给异常数。
异常对象包含的常用方法:

  • getMessage():返回该异常的详细描述字符;
  • printStackTrace():将该异常对象的跟踪栈信息输出到标准错误输出;
  • printStackTrace(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();
        }

    }

java中的异常处理机制_第3张图片

使用finally回收资源

有些时候,程序在try块里打开一些物理资源(如:数据库连接,网络连接,磁盘文件),这些物理资源都必须显式的回收。
为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try和catch快中执行了return语句,finally块总会被执行。

异常处理语法中只有try块是必须的,catch块和finally块是可选的,两者可以同时出>现,也可以只出现一个,但不能都没有。

    public static void main(String[] args) {
        FileInputStream fis=null;
        try {
            fis = new FileInputStream("a.txt");
        }catch (IOException ioe){
            System.out.println(ioe.getMessage());//a.txt (系统找不到指定的文件。)
            return; //强制方法返回
            //System.exit(1);//退出虚拟机
        }finally {
            if(fis!=null){
                try {
                    fis.close();
                }catch (IOException ioe){
                    ioe.printStackTrace();
                }
            }
            System.out.println("执行finally块里的资源回收");//执行finally块里的资源回收
        }
    }

java中的异常处理机制_第4张图片
上面的方法中即使执行了return还是可以看到finally块中的代码被执行。
但是如果将return注释,而将System.exit(1)打开注释会看到虚拟机直接退出,finally块没有被执行。
注意
如果执行try或catch块遇到return,方法不会直接退出,而是去找是否有finally块,如果有先执行finally块,再返回执行try或catch中的return。如果finally块里还有return则会直接结束方法,就不会返回到try护catch执行return。所以尽量避免finally块里使用return或throw等导致方法终止的语句,否则会出现一些奇怪的情况。

java7的自动关闭资源的try语句

在前面程序看到当程序使用finally块关闭资源时,程序显得异常臃肿;
java7的出现改变了这种局面,增强了try语句的功能,它允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,此处的资源是指那些在程序结束时显式关闭的资源。
为了保证try语句可以正常的关闭资源,这些资源实现类必须实现AutoCloseable或Closeable接口。

    public static void main(String[] args) throws IOException {
        try (
                BufferedReader bufferedReader = new BufferedReader(new FileReader("a.txt"));
                PrintStream printStream = new PrintStream(new FileOutputStream("b.txt"));
                ){
            System.out.println(bufferedReader.readLine());
            printStream.println("庄生晓梦迷蝴蝶");
        }

    }

自动关闭资源的try语句相当于包含了隐式的finally块。

Checked异常和Runtime异常体系

Java的异常被分为两大类:Checked(编译时)异常和Runtime(运行时)异常。所有的RuntimeException类及子类的实例被称为Runtime异常;不是RuntimeException类及子类的异常则被称为Checked异常。
对于Checked异常处理的方式有两种:

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

使用throws声明抛出异常

使用throws声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果main方法也不知道如何处理,则抛给JVM处理。JVM对异常的处理方法是打印跟踪栈信息,并中止程序运行。
throws声明抛出只能在方法签名中使用,throws可以声明抛出多个异常类,由逗号隔开。一旦抛出该异常,就不用使用try…catch来捕获了。

使用throws声明抛出异常有一个限制:子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。

使用Checked存在的不便之处:

  • 对于程序中的Checked异常,java要求必须显式捕获并处理该异常,或者显式声明抛出该异常,这样增加了编程复杂度;
  • 如果在方法显式声明抛出Checked异常,将会导致方法签名与异常耦合,如果该方法重写了父类的方法,则抛出的异常还会受到被重写方法所抛出异常的限制。

使用throw抛出异常

抛出异常

当程序出现错误时,系统会自动抛出异常;除此之外,java也允许程序自行抛出异常,自行抛出异常用throw语句来完成注意不是前面的throws,两者有区别。
如果需要在程序中自行抛出异常,则应使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一次异常实例。
不管是系统自动抛出的异常还是程序员手动抛出的异常,java运行时环境对异常的处理没有任何差别。

    public static void main(String[] args) throws Exception {
        //输入字符串长度不超过5,否则引发异常
        Scanner sc = new Scanner(System.in);
        System.out.print("请输入长度不超过5的字符串:");
        String s=sc.next();
        if(s.length()<5){
            System.out.println(s);
        }else {
            throw new Exception("字符串长度超度5");
        }
    }

java中的异常处理机制_第5张图片

如果throw语句抛出的异常是Checked异常,则该throw语句要么处于try显式捕获异常,要么放在一个带throws声明抛出的方法中;如果throw语句抛出的异常时Runtime异常,则该语句无需放在try块里,也无需放在throws声明抛出的方法中,程序既可以显式的捕获异常,也可以完全不理会该异常。

    public static void main(String[] args) {
        try {
            throwChecked(3);
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
        throwRuntime(3);
    }


    public static void throwChecked(int i) throws Exception {
        if(i>0){
            //自行抛出Exception异常
            throw new Exception("a的值大于0,不符合要求");
        }
    }
    public static void throwRuntime(int i){
        if(i>0){
            //自行抛出RuntimeException异常,既可以显式捕获,也可以完全不理会,把异常交给该方法的调用者处理
            throw new RuntimeException("a的值大于0,不符合要求");
        }
    }

自定义异常类

用户自定义异常类都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。定义异常类时需要两个构造器:一个是无参构造;另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息。

public class DefineException extends Exception {
    //无参构造器
    public DefineException(){};
    //带一个字符串参数的构造器
    public DefineException(String str){
        super(str);
    }
}

如果需要自定义Runtime异常,只需将继承父类Exception改成RuntimeException即可。

catch和throw同时使用

在实际生活中需要更复杂的处理方法,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以再次抛出异常,让该方法的调用者也能捕获到该异常。

下面的程序中,bid()方法不仅try…catch了异常,而且throws抛出了异常,这样main方法(bid方法的调用者)也可以处理该异常,并将该异常的详细描述信息输出到标准错误输出。

package org.westos.demo8;

public class ExceptionDemo6 {
    private double iniePrice=30.0;
    public  void bid(String bidPrice) throws DefineException {
        double d=0.0;
        try {
             d = Double.parseDouble(bidPrice);
        }catch (Exception e){
            //此处完成本方法中可以对异常执行的修复处理
            //此处仅仅是在控制台打印异常的跟踪信息
            e.printStackTrace();
            //再次抛出自定义异常
            throw new DefineException("竞拍价必须是数值,不能包含其他字符");
        }
        if(iniePrice>d){
            throw new DefineException("竞拍价比起拍价低,不运行竞拍");
        }
        iniePrice=d;
    }

    public static void main(String[] args) {
        ExceptionDemo6 ex = new ExceptionDemo6();
        try {
            ex.bid("df");
        }catch (DefineException de){
            //再次捕获到bid方法中的异常,并对该异常进行处理
            System.out.println(de.getMessage());
        }

    }
}

java7增强的throw语句
在java7之前,像下面的程序,匹配到的异常是Exception,则抛出的异常也是Exception,但是在java7之后编译器会检查throw语句抛出异常的实际类型,所以抛出具体异常 FileNotFoundException。

    public static void main(String[] args) throws FileNotFoundException {
        try {
            new FileOutputStream("a.txt");
        }catch (Exception e){
            e.printStackTrace();
            throw e;
        }
    }

异常链

对于真正的企业级应用而言,常常有严格的分层关系,层与层之间有非常清晰地划分,上层功能的实现严格依赖下层的API,也不会跨层访问。
java中的异常处理机制_第6张图片

当业务逻辑访问持久层出现SQLException异常时,程序不应该把底层的SQLException异常传到用户界面。原因如下:

  • SQLException异常对用户而言毫无帮助;
  • 异常会暴露隐私安全信息;

把底层异常暴露给用户是不负责任的表现,通常的做法是:程序先捕获异常,然后抛出一个新的业务异常,新的业务异常包含了对用户的提示信息,这种处理方式称为异常转译
这种把原始信息隐藏起来,仅向上提供必要的异常提示信息的处理方式,可以保证底层异常不会扩散到表现层,避免向上暴露太多实现细节。这种把捕获一个异常然后接着抛出另一个异常,并把原始异常信息保存下来是一种典型的链式处理,也被称为异常链
从java1.4之后所有Throwable的子类在构造器中都可以接受一个cause对象作为参数。这个cause就用来表示原始异常,这样可以把原始异常传递给新的异常,使得即使在当前位置创建并抛出新的异常,你也能通过这个异常链追踪到异常最初发生的位置。

异常处理规则

成功的异常处理应该实现如下4个目标:

  • 使程序代码混乱最小化;
  • 捕获并保留诊断信息;
  • 通知合适的人员;
  • 采用合适的方式结束异常活动。

不要过度使用异常
主要体现在两个方面:

  • 把异常和普通错误混淆在一起,不再写任何错误处理代码,而是以简单地抛出异常来代替所有错误处理;
  • 使用异常来代替流程控制。
    异常处理机制的初衷是将不可预期异常的处理代码和正常的业务逻辑处理代码分离,因此绝不要使用异常处理来替换正常的业务逻辑判断

不要使用过于庞大的try块

因为try块里的代码过于庞大,业务过于复杂,就会造成try块中出现异常的可能性大大增加,从而导致分析异常原因的难度也增加。正确的做法是把大块的try块分割成多个可能出现异常的程序段落,并把它们放在单独的try块中,分别捕获异常并处理。

避免使用Catch All语句
Catch All语句是指一种异常捕获模块,它可以处理程序发生的所有可能异常。
这种处理方式的不足之处:

  • 所有异常都采用相同的处理方式,这将导致无法对不同的异常分情况处理,如果要分就得在catch块中使用分支语句,这是得不偿失的。
  • 这种捕获方式可能将程序中的错误、Runtime异常等可能导致程序终止的情况全部捕获到,从而压制了异常。

不要忽略捕获到的异常

既然捕获到了异常,就要进行处理,否则程序除了错误所有人看不到任何异常。
应采取适当措施:

  • 处理异常,对异常进行合适的修复,然后绕过异常发生的地方继续执行。
  • 重新抛出异常,把当前运行环境下能做的事情尽量做完,然后进行异常转译,把异常包装成当前层的异常,重新抛出给上层调用者。
  • 在合适的层处理异常。

你可能感兴趣的:(Java语言)