Java核心技术基础知识学习之异常处理


九、异常处理

异常机制可以使程序中的异常处理代码和正常业务代码分离,提高程序的健壮性。Java 的异常机制主要依赖trycatchfinallythrowthrows 五个关键字,其中 try 关键字后紧跟一个花括号括起来的代码块(花括号不可省略),简称 try 块,里面放置可能出错的代码。catch 后对应的异常类型和一个代码块,用于表明该 catch 块用于处理这种类型的代码块。多个 catch 块后还可以跟一个 finally 块,finally 块用于回收在 try 块里打开的物理资源,异常机制会保证 finally 块总被执行。
throws 关键字主要在方法签名中使用,用于声明该方法可能抛出的异常;而 throw 用于抛出一个实际的异常,throw 可以单独作为语句使用,抛出一个具体的异常对象。
Java 将异常分为两种:Checked 异常Runtime 异常,Java 认为 Checked 异常都是可以在编译阶段被处理的异常,所以它强制程序处理所有的 Checked 异常;而 Runtime 异常则无须处理。

9.1 异常处理机制

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

9.1.1 使用 try…catch 捕获异常

Java 将系统的业务实现放在 try 块中定义,所有的异常处理逻辑放在 catch 块中进行处理。

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

如果指定 try 块中的业务逻辑代码出现异常,系统自动生成一个异常队形,该异常对象被提交给 Java 运行时环境,这个过程被称为抛出(throw)异常。当 Java 运行时环境受到异常对象时,会自动寻找能处理该异常对象的 catch 块:

  • 如果找到合适的 catch 块,则把该异常对象交给该 catch 块处理,这个过程称为捕获(catch)异常;
  • 如果 Java 运行时环境找不到捕获异常的 catch 块,则运行时环境终止,Java 程序也将退出;

不管程序代码块是否处于 try 块中,甚至 catch 中的代码,只要执行改代码块出现异常,系统总会自动生成一个异常对象。如果程序没有为这段代码定义任何的 catch 块,则 Java 运行环境无法找到处理该异常的 catch 块,程序退出。

9.1.2 异常类的继承体系

Java 运行时时环境接收到异常对象后,会依次判断该异常对象是否是 catch 块后异常类或其子类的实例。如果是的话 Java 运行时环境将调用该 catch 块来处理该异常,否则再次拿该异常对象和下一个 catch 块里的异常类进行比较。
try 块后可以有多个 catch 块,这是针对不同异常类提供不同的异常处理方式,且无需使用if...else判断异常类型。Java 程序进入 catch 块后将不会向下执行其他 catch 块,判断ex instanceof EceptionClass == true。除非在循环中使用 continue 开始下一次循环,下一次循环又重新运行 try 块,这样多个 catch 块才可能被执行。

try 块和 catch 后的花括号不可省略,需要注意的是:try 块里声明的变量是代码块内局部变量,只在 try 块中有效,在 catch 块中不能访问该变量。

常见的异常类如下:
Exception

Java 提供了丰富的异常类,这些异常类之间有严格的继承关系。所有的异常情况分为两种:异常(Exception)和错误(Error),它们都继承 Throwable 父类。Error 错误一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。
实际上,进行异常捕获时应该把所有父类异常的 catch 块排在子类异常 catch 块的后面,否则出现编译错误。(先捕获小异常,再捕获大异常)

9.1.3 多异常捕获

从 Java 7 开始,一个 catch 块可以捕获多种类型的异常。

  • 捕获多种类型的异常时,异常类型之间用竖线(|)隔开·;
  • 捕获多种类型的异常时,异常变量有隐式的 final 修饰,因此程序不能对异常变量重新赋值;
public class MutiExceptionTest {
    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("未知异常");
            //捕获一种类型的异常时,异常变量没有 final 修饰
            e = new RuntimeException("test");
        }
    }
}

9.1.4 访问异常信息

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

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

9.1.5 使用 finally 回收资源

程序在 try 块里打开了一些物理资源(例如数据库连接、网络连接和磁盘文件等),这些资源必须显式回收。

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

异常处理语法结构只有 try 块是必须的,catch 块和 finally 块是可选的,但至少出现其一。除非在 try 块、catch 块中调用退出虚拟机的方法(System.exit(1)),否则不管在 try 块、catch 块中执行怎样的代码,异常处理的 finally 块总会被执行。
在通常情况下,不要在 finally 块中使用如 return 或 throw 等导致方法终止的语句,否则会导致 try 块、catch 块中的 return、throw 失效。

public class FinallyFlowTest {
    public static void main(String[] args) throws Exception{
        boolean a = test();
        System.out.println(a);//输出false
    }
    public static boolean test() {
        try {
            //finally 块中包含 return 语句,下面的 return 失效
            return true;
        }
        finally {
            return false;
        }
    }
}

Java 程序执行 tyr 块、catch 块遇到 return 或 throw 语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找该异常处理流程中是否包含 finally 块,执行完 finally 未遇到 return 或 throw 块才跳回去执行 try 块、catch 块里的 return 或 throw 语句。

9.1.6 自动关闭资源的 try 语句

Java 7 增强了 try 语句的功能,允许在 try 关键字后紧跟一对圆括号声明、初始化一个或多个资源,此处的资源是指那些必须在程序结束时必须显式关闭的资源(比如数据库连接、网络连接等),try 语句会在该语句结束时自动关闭该资源。
需要指出的是,为保证 try 语句可以正常关闭资源,这些资源实现类必须实现AutoCloseableCloseable接口,实现这两个接口就必须实现 close() 方法。

Closeable 是 AutoCloseable 的子接口,可以被自动关闭的资源类要么实现 AutoCloseable 接口,要么实现 Closeable 接口。Closeable 接口里的 close() 方法声明抛出了 IOException ,因此它的实现类在实现 Close() 方法时只能声明抛出 IOException 或其子类;AutoCloseable 接口里的 close() 方法声明 抛出了 Exception,因此它的实现类在实现 close() 方法时可以声明抛出任何异常。

public class AutoCloseTest {
    public static void main(String[] args) throws IOException {
        try (
                //声明、初始化两个可关闭的资源
                //try 语句会自动关闭这两个资源
                BufferedReader br = new BufferedReader((new FileReader("AutoCloseTest.java")));
                PrintStream ps = new PrintStream(new FileOutputStream("a.txt"));
        ) {
            //使用两个资源
            System.out.println(br.readLine());
            ps.println("Java 学习");
        }
    }
}

上面程序分别声明、初始化两个 IO 流,由于 BufferedReader、PrintSream 都实现了 Closeable 接口,而且它们放在 try 语句中声明、初始化,所以 try 语句会自动关闭资源。自动关闭资源的 try 语句相当于包含隐式的 finally 块(关闭资源),因此该 try 语句可以没有 catch 块和 finally 块。

Java 7 几乎把所有的资源类(包括文件 IO 的各种类、JDBC 编程的 Connection、Statement 等接口)进行改写,实现 AutoCloseable 或 Closeable 接口。

9.2 Checked 异常和 Runtime 异常体系

Java 的异常被分为两大类:Checked 异常Runtime 异常(运行时异常),所有的 RuntimeException 类及其子类的实例被称为 Runtime 异常;反之则为 Checked 异。如果程序没有处理 Checked 异常,该程序在编译时就会发生错误。
Checked 异常体现了 Java 的设计哲学:没有完善错误处理的代码根本就不会被执行。处理方式有以下两种:

  • 当前方法明确知道如何处理该异常,使用 try…catch 块捕获和修复异常;
  • 当前方法不知道如何处理该异常,在定义该方法时声明抛出异常;

Runtime 异常则更加灵活,无须显式声明抛出。

9.2.1 throws 声明抛出异常

throws 声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理:如果 main 方法也不知道如何处理这种类型的异常,也可以使用 throws 声明抛出异常,该异常将交给 JVM 处理。JVM 对异常的处理方法:打印异常的跟踪栈信息,并终止程序的运行。
throws 声明只能在方法签名中使用,可以声明抛出多个异常类,多个异常类以逗号隔开。**如果某段代码中调用一个带 throws 声明的方法,该方法声明抛出了 Checked 异常,则表明该方法希望它的调用者来处理该异常。**也就是说,调用该方法要么放在 try 块中显示捕获该异常,要么放在另一个 throws 声明抛出的方法中。

import java.io.FileInputStream;
import java.io.IOException;

public class ThrowsTest {
    public static void main(String[] args) throws Exception {
        //因为 test() 方法声明抛出 IOException 异常
        //调用该方法的代码要么处于 try...catch 块中
        //要么处于另一个带 throws 声明抛出的方法中
        test();
    }

    public static void test() throws IOException {
        //因此 FileInputStream 的构造器声明抛出 IOException 异常
        //因此调用 FileInputStream 的代码要么处于 try...catch 块中
        //要么处于另一个带 throws 声明抛出的方法中
        FileInputStream fis = new FileInputStream("a.txt");
    }
}

throws 声明抛出异常有一个限制,就是方法重写中的规则:子类声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类声明抛出的异常不允许比父类方法声明抛出的异常得多。
可见,Checked 异常至少有以下两个不便之处:

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

因此在大部分时间推荐使用 Runtime 异常,而不使用 Checked 异常。尤其当程序需要自行抛出异常时,使用 Runtime 异常将更简洁。Checked 异常也有其优势,它能在编译时提醒程序员必须注意处理该异常或者声明该异常方法调用者来处理。

9.3 throw 抛出异常

程序出现错误时,系统会自动抛出异常;除此之外,Java 也允许程序通过 throw 自行抛出异常。

9.3.1 抛出异常

如果需要在程序中自行抛出异常,则应该使用 throw 语句,throw 语句可以单独使用,其抛出的不是异常类,而是一个异常实例,且每次只能抛出一个异常实例。throw 语句的语法格式如下:

throw ExceptionInstance;

如果 throw 语句抛出的异常是 Checked 异常,则该 throw 语句要么处于 try 块里,显式捕获该异常,要么放在带 throws 声明抛出的方法中,即把该异常交给该方法的调用者处理。而 throw 抛出的 Runtime 异常则没有这些限制。

public class ThrowTest {
    public static void main(String[] args) {
        try {
            //调用声明抛出 Checked 异常的方法,要么显式捕获该异常
            //要么在 main 方法再次声明抛出
            throwChecked(-3);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        //调用声明抛出 Runtime 异常的方法既可以显式捕获该异常
        //也可以不理会该异常
        throwRuntime(3);
    }

    public static void throwChecked(int a) throws Exception {
        if (a > 0) {
            //自行抛出 Exception 异常
            //该代码必须处于 try 块里,或处于带 throws 声明的方法中
            throw new Exception("a 的值大于 0,不符合要求");
        }
    }

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

9.3.2 自定义异常类

在通常情况下,程序很少自行抛出系统异常,因为异常的类名通常也包含该异常的有用信息。因此应该选择合适的异常类,从而明确地描述该异常情况,这种情况下需要抛出自定义异常。
用户自定义异常都应该继承 Exception 基类,如果希望自定义 Runtime 异常,则应该继承 RuntimeException 基类。定义异常类时通常需要提供两个构造器:一个是无参数的构造器;另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息(也就是异常对象的 getMessage() 方法的返回值)。

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

如果需要定义 Runtime 异常,只需将 Exception 基类改为 RuntimeException 基类。

9.3.3 catch 和 throw 同时使用

如果一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可完全处理该异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中完成,所以应再次抛出异常让方法调用者也能捕获到该异常。

public class AuctionTest {
    private double initPrice = 30.0;

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

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

这种 catch 和 throw 结合使用的情况在大型企业级应用中十分普遍。企业级应用对异常的处理通常分成两个部分:

  • 应用后台需要通过日志来记录异常发生的详细情况;
  • 应用还需要根据异常向应用使用者传达某种提示;

9.3.4 异常链

把底层的原始异常直接传给用户是一种不负责任的表现。通常做法是:程序先捕获原始异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息,这种处理方式被称为异常转译

这种把原始信息隐藏起来,仅向上提供必要的异常提示信息的处理方式,可以保证底层异常不会扩散到表现层,可以避免向上暴露太多的实现细节,这完全符合面向对象的封装原则。

这种把不好一个异常然后接着抛出另一个异常,并把原始异常信息保存下来是一种典型的链式处理(23 种设计模式之一:职责链模式),也被称为异常链
在 JDK 1.4 以前,程序员必须自己编写代码来保持原始异常信息。自 JDK 1.4 以后,所有 Throwable 的子类在构造器中都可以接受一个 cause 对象作为参数用来表示原始异常。

9.4 Java 的异常跟踪栈

异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据输出结果开发者可以找到异常的源头,并跟踪到异常一路触发的过程。
面向对象的应用程序运行时,经常会发生一系列方法调用,从而形成方法调用栈。异常的传播则相反:只要异常没有被完全捕获(包括异常没有被捕获或异常被处理后抛出新的异常),异常从发生异常的方法逐渐向外传播,首先传给调用者,该方法调用者再次传给其调用者直到 main 方法,如果 main 方法依然没有处理该异常,JVM 会终止该程序。

虽然 printStackTrace() 方法可以很方便地用于追踪异常的发生情况,可以用来调试程序。但在最后发布的程序中,应该避免使用它;而应该对捕获的异常进行适当的处理,不是简单地将异常的跟踪栈信息打印出来。

9.5 异常处理规则

成功的异常处理应实现以下目标:

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

9.5.1 不要过度使用异常

过度使用异常的负面影响包括:1. 把异常和普通错误混淆在一起,不在编写任何错误处理的代码,而是简单抛出异常代替所有的错误处理;2. 使用异常处理来代替流程控制。
异常处理机制的初衷是将不可预期异常的处理代码和正常业务处理逻辑代码分离,因此绝不要使用异常处理来代替业务逻辑判断

9.5.2 不要使用过于庞大的 try 块

正确做法:将大块的 try 块分割成多个可能出现异常的程序段落,并把它们放在单独的 try 块中,从而分别捕获并处理异常。

9.5.3 避免使用 catch All 语句

Catch All 语句是指一种异常捕获模块,它用来处理程序发生的所有可能异常。

try {
	// 可能引发 Checked 异常的代码
} catch (Throwable t) {
	//进行异常处理
	t.printStackTrace();
}
  • 所有的异常采用相同的处理方式,导致无法对不同的异常分情况处理。如果分情况则需要使用分支语句控制,得不偿失;
  • 这种捕获方式可能将程序中的错误、Runtime 异常等可能导致程序终止的情况全部捕获,从而压制异常。

9.5.4 不要忽略捕获的异常

catch 块应该处理并修复异常,重新抛出异常(进行异常转译),或者在合适的层处理异常。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值