疯狂Java讲义 第十章 Java异常处理

jefxff 153,565 2020-04-10

1. 异常概述

  1. Java的异常机制主要依赖于 try, catch, finally, throw, throws 五个关键字, 其中 try 关键字后紧跟一个花括号扩起来的的代码块, 简称 try块, 它里面放置可能引发异常的代码. catch 后对应异常类型和一个代码块, catch块用于处理这种异常类型的代码块; 多个 catch 块后还可以跟一个 finally 块, finally块 用于回收在 try块 里打开的物理资源, 异常机制会保证 finally块 总是被执行; throws关键字主要在方法签名中使用,用于声明该方法可能抛出的异常类, 而 throw 用于抛出一个实际的异常,throw 可以单独作为语句使用, 抛出一个具体的异常对象
  2. Java异常分为 Checked异常 和 Runtime异常; Checked异常 都是可以在编译阶段被处理的异常, 程序必须强制处理所有的 Checked异常, 而 RUntime异常则无需处理

2. 异常处理机制

2.1 使用 try...catch 捕获异常

  1. Java的异常处理机制是, 当程序运行出现意外情形时, 系统会自动生成一个 Exception对象 来通知程序,从而实现 "业务功能实现代码" 和 "错误处理代码" 分离

  2. 抛出异常
    如果执行 try块 里的业务逻辑代码时出现了异常, 系统会自动生成一个异常对象, 该异常对象提交给Java运行时环境, 这个过程叫做 抛出(throw)异常

  3. 捕获异常
    当Java运行时环境收到异常对象时, 会寻找能处理该异常对象的 catch块, 如果找到合适的 catch块,则把该异常对象交给该catch块处理, 这个过程叫做捕获(catch)异常, 如果Java运行时环境找不到异常的catch块, 则运行时环境终止, Java程序也将退出先捕获小异常, 再捕获大异常

  4. 语法格式

try {
    // 业务实现代码
}
catch (Exception e){
    // 所有异常处理逻辑放在 catch 块中进行处理
}

2.2 异常类的继承体系

  1. Java的所有非正常情况分两种: 异常(Exception)和错误(Error), 他们都继承自 Throwable 父类
  2. Error错误, 一般是指与虚拟机相关的问题, 如系统奔溃, 虚拟机错误, 动态链接失败, 这种错误无法恢复,且不可能捕获, 所以不能试图使用 catch 来捕获Error对象, 在定义方法时, 也无须在其 throws 子句中声明该方法可能抛出的 Error 及其任何子类
  3. Exception类对应的catch块应该排在其他所有catch块的后面, (原因是: Exception catch块如果排在第一位那么别的catch块就不会得到执行,因为所有的异常对象都是Exception或其子类的实例)且所有父类异常的catch块都应该排在子类异常catch块的后面(记: 先处理小异常, 再处理大异常)

示例代码

// DivTest.java 
public class DivTest{
    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 ie) {
            System.out.println("数组越界: 运行程序时输入的参数个数不够");
        }
        catch (NumberFormatException ne) {
            System.out.println("数字格式异常: 程序只能接收整数参数");
        }
        catch (ArithmeticicException ae) {
            System.out.println("算术异常");
        }
        catch (Exception e) {
            System.out.peintln("未知异常");
        }
    }
}

2.3 Java7 提供的多异常捕获

  1. Java7之后, 每个catch块中可以捕获多种类型的异常
  2. 格式: catch 后的括号中多种异常类型之间使用竖线(|) 隔开
  3. 捕获多种类型的异常时, 异常变量有隐式的final修饰, 因此程序不能对异常变量重新赋值(但是捕获一种异常时, 异常变量并没有final修饰, 所以可以重新赋值)

示例代码

// MultiExceptionTest.java 
public class MultiExceptionTest {
    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
                | ArithmeticicException ie){
            System.out.println("程序发生了数组越界, 数字格式异常, 算数异常之一的异常");
            // 捕获多异常时, 异常变量默认有final修饰, 所以下面对 ie 赋值的代码错误
            // ie = new ArithmeticicException("test");
        }
        catch(Exception e) {
            System.out.println("未知异常");
            // 捕获一种类型的异常时, 异常变量没有final 修饰, 所以下面对 e 变量赋值正确
        }
    }
}

2.4 访问异常信息

  1. 通过访问 catch块 后的异常形参来获取异常对象的相关信息
  2. 当Java运行时决定调用某个catch块来处理该异常对象时, 会将异常对象赋给 catch块 后的异常参数, 程序通过该参数来获取异常的相关信息
  3. 异常对象的常用方法:
    • getMessage(): 返回该异常的详细描述字符串
    • printStackTrace(): 将该异常的跟踪栈信息输出到标准错误输出
    • printStackTrace(PrintStream s): 将该异常的跟踪栈信息输出到指定输出流
    • getStackTrace(): 返回该异常的跟踪栈信息

示例代码

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

2.5 使用 finally 回收资源

  1. finally块用于关闭在try中打开的物理资源(如:数据库连接, 网络连接, 磁盘文件), finally块总是被执行(除非在 try块 或 catch块 中掉用了 System.exit() 来退出虚拟机)
  2. Java的垃圾回收机制不会回收任何物理资源, 垃圾回收机制只能回收堆内存中对象所占用的内存
  3. 异常处理结构语法中只有 try块 是必需的, catch块 和 finally块 是可选的, 但 catch块和finally块至少出现其中之一, 也可以同时出现, 捕获父类异常的catch块必须位于捕获子类异常块的后面, 但不能只单独只有try块, 多个catch块必须位于try块的后面, finally块必须位于所有catch块的后面
  4. 不要再finally块中使用return或throw等导致方法终止的语句, 因为一旦在 finally块中使用了 return或throw语句, 将会导致try块, catch块中的return, throw语句失效
  5. 语法格式:
try{
    // 业务实现代码
}
catch(SubException e) {
    // 异常处理块1
}
catch(SubException2 e) {
    // 异常处理块2
}
finally {
    // 资源回收
}

示例代码

// FinallyTest.java
import java.io.FileInputStream;
import java.io.IOException;
// 
public class FinallyTest {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("a.txt");
        }
        catch(IOException ioe) {
            System.out.println(ioe.getMessage());
            // return 语句强制方法返回
            return;
            // 使用exit退出虚拟机
            // System.exit(1);;
        }
        finally {
            // 关闭磁盘文件, 回收资源
            if (fis != null) {
                try {
                    fis.close();
                }
                catch(IOException ioe) {
                    ioe.printStackTrace();
                }
            }
            System.err.println("执行finally块回收资源!");
        }
    }
}

2.6 异常处理嵌套

  1. 在try块, catch块或finally块中包含完整的异常处理流程的情形被称之为异常处理嵌套; 异常处理流程代码可以放在任何能方可执行代码的地方, 因此完整的异常处理流程即可放在 try块中, 也可放在catch块或finally块中

2.7 Java7的自动关闭资源的try语句

  1. Java7增强了try语句的功能, 它允许在try关键字后紧跟一对圆括号, 圆括号可以声明, 初始化一个或多个资源,此处的资源指的是那些必须在程序结束时显式关闭的资源(比如数据库连接, 网络连接等), try语句在该语句结束时自动关闭这些资源
  2. 为了保证 try 语句可以正常关闭资源, 这些资源实现类必须实现 AutoClosrable 或 Closeable 接口, 实现这两个接口就必须实现close()方法
  3. Closeable 是 AutoCloseable 的子接口, Closeable 接口里的 close() 方法声明只能抛出IOException, 因此Closeable 的实现类在实现 close() 方法时只能声明抛出 IOException 或其子类; 但是 AutoCloseable 接口里的 close() 方法声明抛出的是 Exception, 因此 AutoCloseable 的实现类在实现close()方法时可以声明抛出任何异常
  4. 自动关闭资源的 try 语句相当于包含了隐含式的 finally块 (这个finally块用于关闭资源), 因此这个 try 语句可以既没有catch块, 也没有finally块

示例代码

package how2j;
// 
import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
// 
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("庄生晓梦迷蝴蝶");
        }
    }
}

3. Checked 异常 和 Runtime 异常体系

  1. Java的异常被分为两大类: Checked 异常和 Runtime 异常(运行时异常), 所有的 RuntimeException 类及其子类的实例被称为 Runtime 异常; 不是RuntimeException 类及其子类的已成实例则被称之为 Checked 异常
  2. 只有 Java 语言提供 Checked 异常, 因为Java认为Checked异常都是可以被处理(修复)的异常, 所以Java程序必须显式处理 Checked 异常, 如果不处理 Checked 异常, 则报编译错误
  3. Checked 异常的两种处理方式:
    1. 当前方法明确知道如何处理该异常, 程序应该使用 try...catch 块来捕获该异常, 然后在对应的catch块中修复该异常
    2. 当前方法不知道如何处理该异常, 应该在定义该方法时声明抛出该异常

3.1 使用 throws 声明抛出异常(抛出异常类)

  1. throws 抛出异常思路: 当前方法不知道如何处理这种类型的异常, 该异常应该由上一级调用者处理; 如果 main 方法也不知道如何处理这种类型的异常, 也可以使用 throws 声明抛出异常, 将该异常交给JVM虚拟机处理
  2. throws 声明抛出只能在方法签名中使用, throws 可以声明抛出多个异常类, 多个异常类之间使用逗号(,)隔开,一旦使用 throws 语句声明抛出了该异常, 程序就无须使用 try...catch 块来捕获该异常了
  3. 如果一段代码调用了一个带 throws声明的方法, 该方法声明抛出了 Checked 异常, 则表明该方法希望它的调用者来处理该异常, 也就是说, 调用该方法时要么放在 try块中显式的捕获异常, 要么放在另一个带throws声明抛出的方法中(例子1)
  4. 记住: 方法重写时 "两小" 中规定: 子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,
  5. 语法格式: throws ExceptionClass1, ExceptionClass2...
  6. Checked 异常的不便之处:
    1. 对于程序中的 Checked 异常,Java要求必须显式捕获并处理该异常, 或者显式声明抛出该异常, 增加编程难度
    2. 如果在方法中显式声明抛出Checked异常, 将会导致方法签名与异常耦合, 如果该方法是重写父类的方法, 则该方法
      抛出的异常, 还会受到被重写方法抛出的异常的限制

示例代码

// ThrowsTest2.java
public class ThrowsTest2 {
    public static void test() throws IOException {
        // 因为 FileInputStream 的构造器声明抛出 IOException 异常
        // 所以调用 FileInputStream 的代码要么放在 try...catch 块中, 要么放在另外一个带 throws 声明
        // 抛出的方法中
        FileInputStream fis = new FileInputStream("a.txt");
    }
    // 
    public static void main(String[] args) throws Exception {
        // 因为 FileInputStream 的构造器声明抛出 IOException 异常
        // 所以调用 FileInputStream 的代码要么放在 try...catch 块中, 要么放在另外一个带 throws 声明
        // 抛出的方法中
        test();
    }
}

4. 使用 throw 抛出异常(抛出异常实例)

4.1 抛出异常

  1. 如果需要在程序中自行抛出异常, 则应该使用 throw 语句, throw 可单独使用, throw 语句抛出的不是异常类, 而是异常实例, 而且每次只能抛出一个异常实例
  2. 语法格式: throw ExceptionInstance;
  3. 如果 throw 语句抛出的异常是 Checked 异常, 则该throw 语句要么放在 try 块里, 显式捕获该异常, 要么放在一个带throws 声明抛出的方法中, 即将该异常交给该方法的调用者来处理; 如果 throw 语句抛出的异常是 Runtime 异常, 则该语句无须放在 try 块里, 也无须放在带 throws 声明抛出的方法中; 程序既可以显式使用 try...catch 来捕获并处理该异常, 也可以完全不理会该异常, 把该异常交给该方法调用者处理
  4. 自行抛出 Runtime 异常比自行抛出 Checked 异常的灵活性更好

示例代码

// ThrowTest.java
public class ThrowTest {
    public static void main(String[] args) {
        try {
            // 调用声明抛出 Checked 异常的方法, 要么放在 try...catch 中, 要么放在另一个有 throws 抛出的方法中
            // 即: 在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, 不符合要求");
        }
    }
}

4.2 自定义异常类(记住例子的格式)

  1. 自定义异常类都应继承 Exception 基类, 如果希望自定义 Runtime 异常, 则应该继承 RuntimeException 基类
  2. 定义异常类是通常需要提供两个构造器, 一个是无参数的构造器, 另一个是带一个字符串参数的构造器, 这个字符串将作为该异常对象的描述信息(也就是 getMessage() 方法的返回值)
  3. 注意: 创建自定义异常类都可以采用与如下例子相似的代码完成, 只需更改 AuctionException 异常的类名即可,让该异常类的类名可以准确描述该异常

示例代码

// AuctionException.java
// 如果自定义 Runtime 异常, 只需将 Exception 替换成 RuntimeException 基类即可
public class AuctionException extends Exception {
    public AuctionException() {}
    public AuctionException(String msg) {
        super(msg);
    }
}

4.3 catch 和 throw 同时使用

  1. 异常处理的方式:
    1. 在出现异常的方法内捕获并处理异常, 该方法的调用者将不能再次不过该异常
    2. 在方法的签名中声明抛出该异常, 将该异常完全交给方法调用者处理
  2. 实现多个方法协同处理同一个异常的情形: 可以在catch块中结合throw语句来完成
  3. 企业级应用对异常的处理通常分两部分:
    1. 应用后台需要通过日志来记录异常发生的详细情况
    2. 应用还需要根据异常向与应用使用者传达某种提示

示例代码

// AuctionTest.java
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("竞拍加比起拍价低, 不允许竞拍!");
        }
        initPrice = d;
    }
    public static void main(String[] args) {
        AuctionTest at = new AuctionTest();
        try {
            at.bid("df");
        } catch (AuctionException ae) {
            // 再次捕获到 bid() 方法中的异常, 并对异常进行处理
            System.out.println(ae.getMessage());
        }
    }
}

4.4 Java7 增强的 throw 语句

  • Java7开始, Java编译器会执行更细致的检查, Java编译器会检查 throw 语句抛出异常的实际类型, 因此在方法签名中只要声明对应的异常类即可

4.5 异常链

  1. 应用结构: 表现层(用户界面) -- API --> 中间层(实现业务逻辑) -- API --> 持久层(保存数据)
  2. 如果 中间层访问持久层出现 SQLException 异常时, 程序不应该将 SQLException 异常传给表现层(原因:1.没必要;2.传给表现层可能暴露不安全的信息); 通常的做法是: 程序先捕获原始异常, 然后抛出一个新的业务异常, 新的业务异常中包含了对用户的提示信息, 这种处理方式叫做异常转译
  3. 这种把捕获一个异常接着抛出另外一个异常, 并把原始异常信息保存下来是一种典型的链式处理(23种设计模式之一: 职责链模式), 也被称之为 "异常链"

示例代码

// 需求: 实现工资计算的方法
/**
 * 1. 自定义了 SalException 的异常类
 * 2. 做法就是在catch块中接受到SQlException异常类之后, 在catch块中抛出重写后的 SalException 异常
 */
// 
// 创建自定义SalException 异常类
public class SalException extends Exception {
    public SalException() {}
    public SalException(sqle) {
        super(sqle);
    }
    // 创建一个可以接收Throwable 参数的构造器
    public SalException(Throwable t) {
        super(t);
    }
}
// 
// 异常链模式方法
public calSal() throws SalException {
    try {
        // 实现结算工资的业务逻辑
    } catch (SQLException sqle) {
        // 把原始异常记录下来, 留给管理员
        // ...
        // 下面异常中的message就是对用户的提示  -------------> 用户无法查看底层异常
        throw new SalException("访问底层数据库出现异常"); 
        // 
        // 下面异常中的sqle就是原始异常 ---------------> 可以追溯到底层的异常
        throw new SalException(sqle);
    // 
    } catch (Exception e) {
        // 把原始异常记录下来, 留给管理员
        // ...
        // 
        // 下面异常中的message就是对用户的提示  -------------> 用户无法查看底层异常
        throw new SalException("访问底层数据库出现异常");
        // 
        // 下面异常中的e就是原始异常 ---------------> 可以追溯到底层的异常
        throw new SalException(e);
    }
}

5. Java 的异常跟踪栈

  1. 异常对象的 printStackTrace() 方法用于打印异常的跟踪栈信息, 根据 printStackTrace() 方法输出结果可以找到异常的源头
  2. Java的异常跟踪栈被分解成一系列的方法调用, 这是因为: 实现更好的重用性, 将每个可重用的代码单元定义成方法, 将复杂任务逐渐分解为更易管理的小型子任务

6. 异常处理规则

  1. 异常处理的目标:
    1. 使程序代码混乱最小化
    2. 捕获并保留诊断信息
    3. 通知合适的人员
    4. 采用合适的方式结束异常活动
  2. 异常处理的初衷:
    异常处理的初衷是将不可预期异常的处理的代码和正常的业务逻辑代码分离, 因此绝不要使用异常处理来代替正常的业务逻辑判断

6.1 不要过度使用异常

  1. 把异常和普通错误混淆在一起, 不再编写任何错误处理代码, 而是以简单地抛出异常来代替所有的错误处理
  2. 使用异常处理来代替流程控制

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

  • 把大段的 try 块分割成多个可能出现异常的程序段落, 并把他们放在单独的 try 块中, 从而分别捕获并处理异常

6.3 避免使用 Catch All 语句

  1. 所有的异常都采用相同的处理方式, 这将导致无法对不同的异常分情况处理, 如果分情况处理, 则需要在 catch 块中使用分支语句进行孔子, 这种做法得不偿失
  2. 这种捕获方式可能将程序的错误, Runtime 异常等可能导致程序终止的情况全部捕获到, 从而 "压制" 了异常

6.4 不要忽略捕获到的异常

  • catch 块中可以做的事情:
    1. 处理异常;
    2. 重新抛出异常;
    3. 在合适的层处理异常

# Java