异常_演练
参考:
- 韩顺平Java
- Java程序设计教程(洪)
- Java核心技术卷1
- 廖雪峰的官方网站
异常(Exception)
异常对应的英文单词是Exception
(一般情况以外的人(或事物);例外的事物)
内容
- 异常的概念
- 异常的层次结构(★★★)
- 非检查型异常与检查型异常(★)
- 捕获异常(★)
- 自定义异常与抛出异常
引入
课本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最上面的代码,对原始代码进行修改
- 找到可能出现异常的代码
- 把这段代码放到
try
语句块中 - 在
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
方法、单词拼错、大写的写成小写等等,都不是异常。
执行过程出现的异常可分为两大类:
Error
(错误):Java虚拟机无法解决的严重问题,如JVM内部错误、资源耗尽等。Error是致命的,会导致程序崩溃。Exception
(异常):由于编程错误或偶然的外部因素导致的一般性的问题,可以使用针对性的代码进行处理。如除数为0、读取不存在的文件、网络连接中断等
如果把程序比作一个人,出现Error
(错误),就相当于癌症晚期,没治了,只能挂掉,最多只能做到通知用户。总之,Error
是致命的。
异常的层次结构(★★★)
每当发生异常时,会生成一个代表异常的对象,创建异常对象要基于现有的异常类。
Java中的所有异常类都是Throwable
的子类。包括上述的ArithmeticException
。(可通过查看源码验证)
在异常类的体系结构中,最顶层是Throwable
类,下一层立即分成两个分支:Error
和Exception
。(上文提到的)
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
派生),必须处理!
检查型异常的处理
要想让上面代码顺利编译、正常运行,有两种办法:
try/catch
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/catch
或throws
- 存在更优雅的处理方式: 引入了如
Option
、Result
等数据结构,或者使用函数式编程的方式处理异常,更加优雅。
小结
在Java中,有些异常不处理是可以的(非检查型),有些异常强制必须处理(检查型异常)。
检查型异常有两种处理办法:一,try/catch。二,通过throws声明这个方法可能抛出异常。
捕获异常
try/catch
解析
如果发生了某个异常,但没有在任何地方捕获这个异常,程序就会终止。
上文已经使用了最简单的try
语句块:
try {
//
} catch (异常类型 e) {
// 针对该类型异常的处理代码
}
如果try
中任何代码抛出了catch
中指定的一个异常类,那么程序会
- 跳过try中的其余代码
- 执行对应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/catch
、finally
(实际开发经常会用)
懂得thows
和throw
自定义异常首先要熟悉异常的层次结构,在Java中要特别区分检查型异常和非检查型异常