Day13 异常处理
学习目标
- 能够说出异常出现的原因
- 能够说出异常的继承体系
- 能够说出异常的分类
- 能够使用异常处理语句解决代码中的异常
- 能够使用自定义异常处理程序中的问题
1. 概述
在使用计算机语言进行项目开发的过程中,即使程序员把代码写得尽善尽美,在系统的运行过程中仍然会遇到一些问题,因为很多问题不是靠代码能够避免的,比如:客户输入数据的格式,读取文件是否存在,网络是否始终保持通畅等等。
异常的出现就是为了记录程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止的情况。在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。
异常出现的目的是为了保证程序的健壮性。代码出现问题时,曝出异常,程序员解决这些异常以后再继续向下执行。如果不抛出异常,程序就会在错误的基础上继续执行,这样更加危险。
2. 异常体系
异常机制其实是帮助我们找到程序中的问题,异常的根类是java.lang.Throwable
,其下有两个子类:java.lang.Error
与java.lang.Exception
,平常所说的异常指java.lang.Exception
。
- Error:严重错误Error,无法通过处理的错误,只能事先避免,例如常见的
StackOverFlowError
和OutOfMemoryError
。 - Exception:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是需要处理的。
Throwable中的常用方法:
-
public void printStackTrace()
:打印异常的详细信息。包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。
-
public String getMessage()
:获取发生异常的原因。提示给用户的时候,就提示错误原因。
-
public String toString()
:获取异常的类型和异常描述信息(不用)。
3. 异常分类
我们平常说的异常就是指Exception,因为这类异常一旦出现,我们就要对代码进行更正,修复程序。
异常(Exception)的分类:根据在编译时期还是运行时期去检查异常?
- 编译时期异常:checked异常。在编译时期,就会检查,如果没有处理异常,则编译失败。(如日期格式化异常)
- 运行时期异常:runtime异常。在运行时期,检查异常.在编译时期,运行异常不会编译器检测(不报错)。(如数组索引越界异常)
4. 异常产生过程
先运行下面的程序,程序会产生一个数组索引越界异常ArrayIndexOfBoundsException。我们通过图解来解析下异常产生的过程。
class ArrayTools {
// 对给定的数组通过给定的角标获取元素。
public static int getElement(int[] arr, int index) {
int element = arr[index];
return element;
}
}
public class ExceptionDemo {
public static void main(String[] args) {
int[] arr = { 34, 12, 67 };
int num = ArrayTools.getElement(arr, 4)
System.out.println("num=" + num);
System.out.println("over");
}
}
5. 异常处理语句
Java异常处理的五个关键字:try、catch、finally、throw、throws
5.1 捕获异常
使用try...catch
语句来捕获程序运行过程中出现的异常。
try{
// 编写可能会出现异常的代码
}catch(异常类型 e){
// 处理异常的代码
// 记录日志/打印异常信息/继续抛出异常等
}
注意:try…catch是一个完整的语句,不能分开使用。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd HH:MM:SS");
try {
Date date = sdf.parse("2021 01 01 12:10:15");
// 如果上面 parse 的代码出问题了,下面这行代码将不会被执行!
System.out.println("解析后的日期是" + date);
} catch (ParseException e) {
System.out.println("解析错误!");
e.printStackTrace();
}
5.2 finally代码块
因为异常会引发程序跳转,导致有些语句执行不到。但是如果有一些特定的代码无论异常是否发生,都需要执行,就可以讲这些代码写入到finally代码块里。
什么时候的代码必须最终执行?当我们在try语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),我们都得在使用完之后,最终关闭打开的资源。
finally的语法:
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}finally{
// 程序代码
}
注意:finally不能单独使用。
比如在我们之后学习的IO流中,当打开了一个关联文件的资源,最后程序不管结果如何,都需要把这个资源关闭掉。
FileInputStream fis = null;
try {
fis = new FileInputStream("out.txt");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
finally注意事项:如果方法的finally里有return语句,会将前面的return语句覆盖。
5.3 throw抛出异常
在编写程序时,我们必须要考虑程序出现问题的情况。如果程序在运行的过程中出现了问题,我们可以使用throw
关键字将异常抛出。
语法格式:
throw new 异常类名(参数);
示例代码:
private static void divide(int a) {
if (a == 0) {
//System.out.println("除数不能为0");
//return; //可以使用 return 语句结束代码的执行
throw new RuntimeException("除数不能为0");
}
double b = 1.0 / a;
System.out.println("您传入的数字的倒数是" + b);
}
5.4 throws声明异常
throws
关键字将问题标识出来,报告给调用者。如果方法内通过throw抛出了编译时异常,那么必须通过throws进行声明,让调用者去处理。
关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常)。
public class Test {
public static void main(String[] args) {
// divide 方法抛出了异常,不能再这样直接调用
// divide(0);
// 此时,再调用 divide 方法,要么使用try...catch语句处理异常,要么就再把异常向上抛出
try {
divide(3);
}catch(Exception e) {
e.printStackTrace();
}
}
// 在函数声明时就必须要使用 throws 关键字声明异常类型
private static void divide(int a) throws Exception {
if (a == 0) {
// 这里使用 throw 抛出了一个编译时异常
throw new Exception("除数不能为0");
}
double b = 1.0 / a;
System.out.println("您传入的数字的倒数是" + b);
}
}
5.5 异常处理注意事项
-
多个异常时的处理方式。
try { FileInputStream fis = new FileInputStream("test.txt"); System.out.println(1 / 0); fis.close(); } catch (FileNotFoundException e) { System.out.println("文件未找到"); e.printStackTrace(); } catch (IOException e) { System.out.println("文件关闭失败"); }catch (Exception e) { // 如果有父类,父类的异常处理语句要写在最下面 System.out.println("出问题了"); }
注意:这种异常处理方式,要求多个catch中的异常不能相同,并且若catch中的多个异常之间有子父类异常的关系,那么子类异常要求在上面的catch处理,父类异常在下面的catch处理。
-
运行时异常被抛出可以不处理。即不捕获也不声明抛出。
-
如果finally有return语句,永远返回finally中的结果,避免该情况。
-
如果父类抛出了多个异常,子类重写父类方法时,抛出和父类相同的异常或者是父类异常的子类或者不抛出异常。
-
父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出。
6. 自定义异常
Java里的异常类型都继承自Exception类,常见的Java内置异常类型有:
- ClassCastException:类型转换异常
- ArrayIndexOutOfBoundsException:数组下标越界异常
- NullPointerException:空指针异常。
- ArithmeticException:算法异常。
- NumberFormatException:数字格式化异常。
- FileNotFoundException:文件未找到异常。
- FileAlreadyExistsException:文件已经存在异常。
6.1 为什么要自定义异常
在Java中,我们会使用throw
关键字抛出异常,通常情况下,我们可以创建Java内置的异常类型对象。但是在有些情况下,Java内置的错误类型可能不能够很精确的描述我们的遇到的异常类型,此时就可以考虑自定义异常。
例如:除数为0的时候,Java里默认抛出的异常类型是ArithmeticException
,并不能够很好的描述我们遇到的异常类型,开发中可能会要求抛出一个ZeroDivisionException
自定义异常。
6.2 如何自定义异常
异常分为两大类,编译时异常和运行时异常。我们在开发中,也可以根据我们的业务逻辑,选择继承的异常类型。
- 自定义编译时异常:自定义异常类需要继承自
Exception
类。 - 自定义运行时异常:自定义异常类需要继承自
RuntimeException
类。 - 根据自己的业务需求,还可以继承
Exception
或者RuntimeException
的子类。
public class Test {
public static void main(String[] args) throws IOException {
test(0);
}
public static void test(int a) {
if (a == 0) {
// 在这里抛出一个自定义的异常
throw new ZeroDivisionError("除数不能为0");
}
double x = 1.0 / a;
System.out.println("计算的结果是" + x);
}
}
// 根据业务需求分析,ZeroDivisionError 继承自 ArithmeticException 会更加合适
class ZeroDivisionError extends ArithmeticException {
private String s;
ZeroDivisionError(String s) {
super(s);
this.s = s;
}
@Override
public String toString() { // 可以选择重写自定义异常的 toString 方法,在打印异常时输出指定的内容
return "程序运行出错了,错误原因是" + this.s;
}
}