异常_演练

参考:

  • 韩顺平Java
  • Java程序设计教程(洪)
  • Java核心技术卷1
  • 廖雪峰的官方网站

异常(Exception)

异常对应的英文单词是Exception(一般情况以外的人(或事物);例外的事物)

内容

  1. 异常的概念
  2. 异常的层次结构(★★★)
  3. 非检查型异常与检查型异常(★)
  4. 捕获异常(★)
  5. 自定义异常与抛出异常

引入

课本P205例7.1A

import java.util.Scanner;

public class Example1 {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        System.out.print("整数a: ");
        int a = sc.nextInt();
        System.out.print("整数b: ");
        int b = sc.nextInt();

        System.out.println("a/b: " + a / b);

        System.out.println("程序继续运行......");
    }
}

理想状态下,用户输入的数据永远是符合规则的,例如,输入 10 除以 5 可以顺利得到 2,并且之后的代码也会顺利地执行。

但现实世界里,总是充斥着各种各样的例外。例如,输入一个 10 和 0 ,这段程序就会直接崩溃:

整数a: 10
整数b: 0
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Example1.main(Example1.java:13)

对该现象做一个分析: 

        // 解析
        // 1 执行 到 a/b 时 , b = 0 , 除数不能为0, 产生( 抛出 ) 一个异常
        // 具体而言 该异常 叫   Arithmetic算术  Exception异常
        // 2 抛出异常后 -> 程序 崩溃 , 后续代码不再执行
        // 3 这样 的程序好吗 ? 仅因用户 输入错误导致 整个系统崩溃 - > 淘宝
        // 4 显然不好 -> 异常 处理 机制

        // 当出现异常时, 一个好的程序
        // 1 至少通知用户 -> 尽量人性化
        // 2 执行一些处理 异常事件 的代码
        // 3 尽量让后面代码能继续执行

        // try/catch语句块

解决方法

Java提供了一种叫作try/catch的语句块,可以帮助我们实现这两点。

怎么用try/catch课本P207最上面的代码,对原始代码进行修改

  1. 找到可能出现异常的代码
  2. 把这段代码放到try语句块中
  3. catch子句中捕获可能出现的异常
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int a = sc.nextInt();
        int b = sc.nextInt();

        // 2写try块
        try {
            System.out.println(a / b); // 1找到可能出现异常的代码

            System.out.println(" 后续 代码 1  执行中"); // 如有异常就被跳过了
        } catch (ArithmeticException e) {
            System.out.println(" 除数不能为 0 ");
        }

        System.out.println(" 后续 代码 2 执行中");
    }
}

再次执行这段程序,就不会再因为除数为0而崩溃:

10
0
 除数不能为 0 
 后续 代码 2 执行中

异常简介

Java中,将程序执行中发生的不正常情况称为异常。(开发过程中的语法错误、逻辑错误不是异常)

例如,经常发现有同学不写main方法、单词拼错、大写的写成小写等等,都不是异常。

执行过程出现的异常可分为两大类:

  1. Error(错误):Java虚拟机无法解决的严重问题,如JVM内部错误、资源耗尽等。Error是致命的,会导致程序崩溃。
  2. Exception(异常):由于编程错误或偶然的外部因素导致的一般性的问题,可以使用针对性的代码进行处理。如除数为0、读取不存在的文件、网络连接中断等

如果把程序比作一个人,出现Error(错误),就相当于癌症晚期,没治了,只能挂掉,最多只能做到通知用户。总之,Error是致命的。

异常的层次结构(★★★)

每当发生异常时,会生成一个代表异常的对象,创建异常对象要基于现有的异常类。

Java中的所有异常类都是Throwable的子类。包括上述的ArithmeticException。(可通过查看源码验证

在异常类的体系结构中,最顶层是Throwable类,下一层立即分成两个分支:ErrorException。(上文提到的)

Error是致命性的、无能为力的严重问题。编程中要重点关注的是可以预防或预先设置处理方法的Exception

Exception可以分为两个分支:RuntimeException(运行时异常)和其它异常。

一般规则是:由编程错误导致的异常属于RuntimeException;如果程序本身没问题,但由于一些不可避免的外部因素导致的异常属于其它异常。

RuntimeException

有句话:“如果出现 RuntimeException 异常,那么一定是你的问题”。

例如,上面例子中的ArithmeticException(算数异常)就属于运行时异常。也就是说,此类异常一般通过调整代码是可以避免的。

        if (b == 0) {
            System.out.println(" 除数不能为 0 ");
        } else {
            System.out.println(a / b);
        }

出现这种异常就是提示你——代码写得有点问题!再比如,常见的数组越界异常ArrayIndexOutOfBoundsException也是运行时异常:

        int[] arr = new int[3];
        System.out.println(arr[5]);

出现该异常实际上是代码没写好,做了不该做的事情。

还有常见的空指针异常NullPointerException

public class Student {
    void study() {

    }
}
public class Test {
    public static void main(String[] args) {
        Student s = null;
        s.study();
    }
}

这是因为你尝试去使用一个没有创建的对象(null意味着对象不存在)。

非检查型异常与检查型异常

上述其实就是非检查型异常,即不进行try/catch也行。还有一种异常,会强迫你必须进行try/catch。被叫做检查型异常,这种异常是Java独有的。

课本P206例7.1B

import java.io.File;
import java.util.Scanner;

public class Example2 {
    public static void main(String[] args) {
        File file = new File("hi.txt"); // 指定要读取的文件
        Scanner sc = new Scanner(file); // 创建文件扫描器,用于读取文件内容
        
        while (sc.hasNextInt()) { // 如果文件中还有整数
            int n = sc.nextInt(); // 读取下一个整数
            System.out.println("读取数字 " + n); // 打印读取的整数
        }
        
        sc.close(); // 关闭扫描器,释放资源
    }
}

PS. 将hi.txt文件放在项目的根目录中,也就是与src文件夹同级的位置

这段代码的功能是从文件 hi.txt 中读取所有整数并逐一打印。

当直接执行这段代码时,会产生一个编译错误:

java: 未报告的异常错误java.io.FileNotFoundException; 必须对其进行捕获或声明以便抛出

意思是:有一个可能的异常没有处理,该异常叫FileNotFoundException,即文件可能找不到,不能通过编译。

为何产生这个编译错误,这是Java独有的一种特色。

此类异常是不能通过调整代码避免,例如hi.txt是否存在,取决于外部环境,而不取决于你的代码。

针对这种情况,为了提高代码的健壮性,Java的设计者引入了检查型(checked)异常的概念,当程序中可能抛出检查型异常时,开发者必须使用try-catch捕获,要么使用throws声明抛出,否则就编译不通过!FileNotFoundException就是一个典型的检查型异常(由IOException派生),必须处理!

检查型异常的处理

要想让上面代码顺利编译、正常运行,有两种办法:

  1. try/catch
  2. throws

try/catch语句块:

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class Example2 {
    public static void main(String[] args) {
        File file = new File("hi.txt"); // 指定要读取的文件
        Scanner sc = null; // 创建文件扫描器,用于读取文件内容
        try {
            sc = new Scanner(file);
        } catch (FileNotFoundException e) {
            
        }

        while (sc.hasNextInt()) { // 如果文件中还有整数
            int n = sc.nextInt(); // 读取下一个整数
            System.out.println("读取数字 " + n); // 打印读取的整数
        }

        sc.close(); // 关闭扫描器,释放资源
    }
}

通过throws声明这个方法可能抛出异常(如果可能抛出多个检查型异常,则需要列出所有的异常类,用逗号分隔):

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class Example2 {
    public static void main(String[] args) throws FileNotFoundException {
        File file = new File("hi.txt"); // 指定要读取的文件
        Scanner sc = new Scanner(file); // 创建文件扫描器,用于读取文件内容

        while (sc.hasNextInt()) { // 如果文件中还有整数
            int n = sc.nextInt(); // 读取下一个整数
            System.out.println("读取数字 " + n); // 打印读取的整数
        }

        sc.close(); // 关闭扫描器,释放资源
    }
}

需要强调的是,对于非检查型异常,例如算数异常、数组越界异常、空指针异常,也可以这样处理,但不是强制性的,即不这么做也能通过编译。

而且这种做法也不推荐。特别是对于上述的几个异常,应该多花时间修正这些错误,而不只是声明这些错误有可能发生。

Java异常层次结构中的检查型异常和非检查型异常

Java 语言规范将派生于 Error类或 RuntimeException类的所有异常称为非检查型(unchecked)异常,所有其他异常称为检查型(checked)异常

如下图,红色的是检查型异常,绿色的是非检查型异常。

检查型异常是一个有争议的设计

有如下事实:

  • 在主流的编程语言中,只有 Java 实现了检查型异常(Checked Exception)的机制
  • 其他大多数编程语言(C++、、C#、Python、JavaScript、Go、Kotlin等)都选择不支持检查型异常
  • Go语言中,甚至没有传统意义上的异常处理机制(即try/catch语句块),而是采用了一种更简单、直接的方式——通过函数的返回值来传递错误信息。

以下是一些反对检查型异常的观点:

  • 代码冗长和复杂: 检查型异常要求在方法签名中声明可能抛出的异常,或者在代码中捕获并处理。这可能导致代码变得冗长,降低可读性。
  • 破坏封装性: 当方法签名中包含检查型异常时,方法的实现细节暴露给了调用者,违反了封装原则。
  • 没有显著提高代码质量: 实践表明,受检异常并没有显著提高代码的质量,反而带来了一些问题。因此,许多语言选择不支持受检异常。
  • 不信任开发者:只有Java会强制开发者进行try/catchthrows
  • 存在更优雅的处理方式: 引入了如 OptionResult等数据结构,或者使用函数式编程的方式处理异常,更加优雅。

小结

在Java中,有些异常不处理是可以的(非检查型),有些异常强制必须处理(检查型异常)。

检查型异常有两种处理办法:一,try/catch。二,通过throws声明这个方法可能抛出异常。

捕获异常

try/catch解析

如果发生了某个异常,但没有在任何地方捕获这个异常,程序就会终止。

上文已经使用了最简单的try语句块:

        try {
            // 
        } catch (异常类型 e) {
            // 针对该类型异常的处理代码
        }

如果try中任何代码抛出了catch中指定的一个异常类,那么程序会

  1. 跳过try中的其余代码
  2. 执行对应catch中的代码

如果没抛出任何异常则跳过catch子句,如果抛出了但不是catch指定的异常,那么这个方法会立即退出。

实际上,在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理:

public class MultiCatchExample {
    public static void main(String[] args) {
        try {
            int result = 10 / 0; // ArithmeticException
            String str = null;
            System.out.println(str.length()); // NullPointerException
        } catch (ArithmeticException e) {
            System.out.println("数学错误:不能除以零!");
        } catch (NullPointerException e) {
            System.out.println("空指针错误:对象未初始化!");
        } catch (Exception e) {
            System.out.println("其他错误:" + e.getMessage());
        }
    }
}

需要注意的是catch语句块是从上往下执行的,一旦匹配到一个就不会再往下执行。这意味着必须先捕获子类异常再捕获父类异常。(课本P209)

finally子句

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class Example2 {
    public static void main(String[] args) {
        File file = new File("hi.txt"); // 指定要读取的文件
        Scanner sc = null; // 创建文件扫描器,用于读取文件内容
        try {
            sc = new Scanner(file);

            while (sc.hasNextInt()) { // 如果文件中还有整数
                int n = sc.nextInt(); // 读取下一个整数
                System.out.println("读取数字 " + n); // 打印读取的整数
            }
        } catch (FileNotFoundException e) {
            System.out.println("文件没找到");
        } finally {
            sc.close();
            System.out.println("关闭扫描器,释放资源");
        }
    }
}

不管是否捕获到异常,finally子句中的代码都会执行。

分析代码下面代码:

work方法什么情况下返回 0?什么情况返回 1?

如果文件存在,finally中的代码是在return前执行还是return后执行?

    private static int work() {
        File file = new File("hi.txt"); // 指定要读取的文件
        Scanner sc = null; // 创建文件扫描器,用于读取文件内容
        try {
            sc = new Scanner(file);

            while (sc.hasNextInt()) { // 如果文件中还有整数
                int n = sc.nextInt(); // 读取下一个整数
                System.out.println("读取数字 " + n); // 打印读取的整数
            }

            return 0;
        } catch (FileNotFoundException e) {
            System.out.println("文件没找到");
        } finally {
            if (sc != null) {
                sc.close();
            }
            System.out.println("关闭扫描器,释放资源");
        }

        return 1;
    }
    
    public static void main(String[] args) {
        System.out.println(work());
    }

自定义异常与抛出异常

自定义异常参考异常层次结构。重点是区分检查型异常和非检查型异常。

可以通过throw关键字抛出异常:

throw new RuntimeException();

练习

教材练习

《Java程序设计教程(洪)》第一版,P217

一:1~8、10

三:2、3、5

其它

在异常处理中,释放资源、关闭文件等应由________语句块处理。

代码

先理解课本例7.1A与7.1B及它们的改进方式

熟悉try/catchfinally(实际开发经常会用)

懂得thowsthrow

自定义异常首先要熟悉异常的层次结构,在Java中要特别区分检查型异常和非检查型异常

posted @ 2024-12-07 21:13  xkfx  阅读(35)  评论(0编辑  收藏  举报