1. 异常
1.1 概述
(1) 认识异常
异常是指程序在运行过程中出现非正常情况。
(2) Java 异常体系结构
所有异常类都是 Throwable
类的子类,它派生出两个子类,Error
类和 Exception
类。
(1)Error 类 : 表示程序无法恢复的严重错误或者恢复起来比较麻烦的错误,例如内存溢出、动态链接失败、虚拟机错误等。应用程序不应该主动抛出这种类型的错误,通常由虚拟机自动抛出。如果出现这种错误,最好的处理方式是让程序安全退出。
(2)Exception 类 : 由Java应用程序抛出和处理的非严重错误,例如文件未找到、网络连接问题、算术错误(如除以零)、数组越界、加载不存在的类、对空对象进行操作、类型转换异常等。Exception类的不同子类对应不同类型的异常。。Exception类又可分为两大类异常:
不受检异常
:也称为 unchecked 异常,包括RuntimeException
及其所有子类。对这类异常并不要求强制进行处理,例如算术异常ArithmeticException等。受检异常
:也称为 checked 异常,指除了不受检异常外,其他继承自Exception
类的异常。对这类异常要求在代码中进行显式处理。
程序中常见异常
异常类名 | 分类 | 说明 |
---|---|---|
Exception | 设计时异常 | 异常层次结构的根类 |
IOException | 设计时异常 | IO异常的根类,属于非运行时异常 |
FileNotFoundException | 设计时异常 | 文件操作时,找不到文件,属于非运行时异常 |
RuntimeException | 运行时异常 | 运行时异常的根类,RuntimeException及其子类不要求处理 |
ArithmeticException | 运行时异常 | 算术运算异常,例如除数为零,属于运行时异常 |
IllegalArgumentException | 运行时异常 | 方法接收到非法参数,属于运行时异常 |
ArrayIndexOutOfBoundsException | 运行时异常 | 数组越界访问异常,属于运行时异常 |
NullPointerException | 运行时异常 | 尝试访问null对象的成员时发生的空指针异常,属于运行时异常 |
ArrayStoreException | 运行时异常 | 数据存储异常,写数组操作时,对象或数据类型不兼容 |
ClassCastException | 运行时异常 | 类型转换异常 |
IllegalThreadStateException | 运行时异常 | 试图非法改变线程状态,例如试图启动一已经运行的线程 |
NumberFormatException | 运行时异常 | 数据格式异常,试图把字符串非法转换成数值 |
上述异常类按照设计时异常和运行时异常进行了分类,并提供了相应的说明。设计时异常通常属于编译时异常,需要在代码中进行处理或声明抛出;而运行时异常通常不要求强制处理,可以选择捕获和处理,也可以由调用者处理,如果没有处理,将会导致程序异常终止。
1.2 Java异常处理机制
1.2.1 异常处理
(1)用 try
、catch
、finally
来捕获和处理异常
try
块中包含可能会抛出异常的代码catch
块用于捕获并处理指定类型的异常finally
块中的代码无论是否发生异常都会被执行,通常用于释放资源或清理操作
(2)用 throw
、throws
来抛出异常
throw
关键字用于手动抛出异常
public void method() throws CustomException {
if (condition) {
throw new CustomException("This is a custom exception");
}
}
throws
关键字用于在方法中声明指定可以抛出的异常类型,表示该方法可能会抛出该类的异常,由调用者来处理
public void method() throws IOException, CustomException {
// 方法体
}
throw
用于具体的异常对象,而throws
用于异常类型的声明。
1.2.2 捕获异常
(1)try-catch 处理异常
public class ExceptionTryCatch {
public static void main(String[] args) {
try {
int i = 1,j = 0,res;
System.out.println("begin");
res = i / j;
System.out.println("end");
} catch (Exception e) {
System.out.println("caught");
e.printStackTrace();
}
System.out.println("over");
}
}
begin
caught
over
java.lang.ArithmeticException: / by zero
at kfm.bases.ErrorAndPxception.ExceptionTryCatch.main(ExceptionTryCatch.java:8)
try-catch语句块首先执行try语句块中的语句,这时可能会出现以下3种情况:
- 如果try语句块中的所有语句正常执⾏完毕,没有发⽣异常,那么catch语句块中的所有语句都将被忽略。
- 如果try语句块在执⾏过程中发⽣异常,并且这个异常与catch语句块中声明的异常类型匹配,那么try语句块中剩下的代码都将被忽略,⽽相应的catch语句块将会被执⾏。匹配是指catch所处理的异常类型与try块所⽣成的异常类型完全⼀致或是它的⽗类。
- 如果try语句块在执⾏过程中发⽣异常,⽽抛出的异常在catch语句块中没有被声明,那么程序⽴即终⽌运⾏,程序被强迫退出。
- catch语句块中可以加⼊⽤⼾⾃定义处理信息,也可以调⽤异常对象的⽅法输出异常信息,常⽤的⽅法如下:
void prinStackTrace()
:输出异常的堆栈信息。堆栈信息包括程序运行到当前类的执行流程,它将输出从方法调用处到异常抛出处的方法调用的栈序列。String getMessage()
:返回异常信息描述字符串,该字符串描述了异常产生的原因,是 printStackTrace() 输出信息的一部分。
(2)try-catch-finally 处理异常
try-catch-finally
语句块组合使用时,无论 try
块中是否发生异常, finally
语句块中的代码总能被执行。
public class TryCatchFinally {
public static void main(String[] args) {
try {
int i = 1,j = 0, res;
System.out.println("bregin");
res = i / j;
System.out.println("end");
} catch (ArithmeticException e) {
System.out.println("caught");
System.out.println(e.getMessage()); // 异常信息描述字符串
e.printStackTrace(); // 输出异常堆栈信息
System.out.println("1");
} finally {
System.out.println("finally");
}
System.out.println("over");
}
}
bregin
caught
/ by zero
java.lang.ArithmeticException: / by zero
at kfm.bases.ErrorAndPxception.TryCatchFinally.main(TryCatchFinally.java:8)
1
finally
over
try-catch-finally语句块执行流程大致分为如下两种情况。
- 如果
try
语句块中所有语句正常执⾏完毕,程序不会进⼊catch
语句块执⾏,但是finally
语句块会被执⾏。 - 如果
try
语句块在执⾏过程中发⽣异常,程序会进⼊到catch
语句块捕获异常,finally
语句块也会被执⾏。
try-catch-finally
结构中 try
语句块是必须存在的,catch
、finally
语句块为可选,但两者⾄少出现其中之⼀。
即使在
catch
语句块中存在return
语句,finally
语句块中的语句也会执行。发生异常时的执行顺序是,先执行catch
语句块中return
之前的语句,再执行finally
语句块中的语句,最后执行catch
语句块中的return
语句退出。
finally
语句块中语句不执行的唯一情况是在异常处理代码中执行了System.exit(1)
,退出 java 虚拟机。
public class TryCatchFinallyExit {
public static void main(String[] args) {
try {
int i = 1,j = 0,res;
System.out.println("begin");
res = i / j;
System.out.println("end");
} catch (ArithmeticException e) {
System.out.println("caught");
e.printStackTrace();
System.exit(1);
} finally {
System.out.println("finally");
}
System.out.println("over");
}
}
begin
caught
java.lang.ArithmeticException: / by zero
at kfm.bases.ErrorAndPxception.TryCatchFinallyExit.main(TryCatchFinallyExit.java:8)
System.exit(1)
是一个 Java
中的方法调用,用于终止当前正在执行的 Java 虚拟机(JVM)
进程。在调用 System.exit(1)
后,JVM
会立即退出,并返回一个指定的退出状态码 (1 在这里表示非正常退出),0 表示正常退出,非零表示非正常退出。
(3)使用多重 catch 处理异常
当一段代码可能引发多种类型的异常时,可以在一个try语句块后面跟随多个 catch
语句块,分别处理不同类型的异常。一旦系统执行了与异常类型匹配的 catch
语句块,并执行其中的一条 catch
语句后,其后的 catch
语句块将被忽略,程序将继续执行紧随 catch
语句块的代码。
catch
语句块的排列顺序必须是从子类到父类,最后一个一般是Exception
类。这是因为在异常处理中,catch
语句块会按照从上到下的顺序进行匹配,系统会检测每个catch
语句块处理的异常类型,并执行第一个与异常类型匹配的catch
语句块。如果将父类异常放在前面,子类异常的catch
语句块将永远不会被执行,因为父类异常的catch
语句块已经处理了异常。
import java.util.InputMismatchException;
import java.util.Scanner;
public class MuchCatch {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.println("计算开始");
int i, j, res;
try {
System.out.println("请输入被除数");
i = input.nextInt();
System.out.println("请输入除数");
j = input.nextInt();
res = i / j;
System.out.println("结果为" + res);
} catch (InputMismatchException e) {
System.out.println("除数和被除数必须为整数");
} catch (ArithmeticException e) {
System.out.println("除数不能为0");
} catch (Exception e) {
System.out.println("其他异常" + e.getMessage());
} finally {
System.out.println("感谢使用本程序");
}
System.out.println("程序结束");
}
}
1.2.3 抛出异常
(1)使用 throws 声明抛出异常
try-catch-finally
处理的是在一个方法内部发生的异常,在方法内部直接捕获并处理。如果在一个方法体内抛出了异常,并希望调用者能够及时地捕获异常,Java
语言中通过关键字 throws
声明某个方法可能抛出的各种异常,以通知调用者。throws
可以同时声明多个异常,之间用逗号隔开。
import java.util.InputMismatchException;
import java.util.Scanner;
public class ThrowsExcepton {
public static void main(String[] args) {
try {
divide();
} catch (InputMismatchException e) {
System.out.println("除数和被除数必须为整数");
} catch (ArithmeticException e) {
System.out.println("除数不能为0");
} catch (Exception e) {
System.out.println("其他异常" + e.getMessage());
} finally {
System.out.println("感谢使用本程序");
}
System.out.println("程序结束");
}
// 通过 throws 声明抛出设计时异常
public static void divide() throws Exception {
Scanner input = new Scanner(System.in);
System.out.println("计算开始");
int i, j, res;
System.out.println("请输入被除数");
i = input.nextInt();
System.out.println("请输入除数");
j = input.nextInt();
res = i / j;
System.out.println("结果为" + res);
}
}
(2)使用 throw 抛出异常
除了系统自动抛出异常外,在编程过程中,有些问题是系统无法自动发现并解决的,如年龄不在正常范围之内,性别输入的不是“男”或“女”等,此时需要程序员而不是系统来自行抛出异常,把问题提交给调用者去解决。在 Java
语言中,可以使用 throw
关键字来自行抛出异常。
throw new Exception("message")
使用
throw
语句抛出异常,让调用者解决异常。
public void divide(int dividend, int divisor) {
if (divisor == 0) {
throw new ArithmeticException("Cannot divide by zero");
} else {
int result = dividend / divisor;
System.out.println("Result: " + result);
}
}
public static void main(String[] args) {
try {
int dividend = 10;
int divisor = 0;
divide(dividend, divisor);
} catch (ArithmeticException e) {
System.err.println("Exception caught: " + e.getMessage());
}
}
Exception caught: Cannot divide by zero
- 如果 throw 语句抛出的异常是 Checked 异常,则该 throw 语句要么处于 try 块⾥,显式捕获该异常,要么放在⼀个带 throws 声明抛出的⽅法中,即把该异常交给该⽅法的调⽤者处理;
- 如果 throw 语句抛出的异常是 Runtime 异常,则该语句⽆须放在 try 块⾥,也⽆须放在带 throws 声明抛出的⽅法中;程序既可以显式使⽤ try…catch来捕获并处理该异常,也可以完全不理会该异常,把该异常交给该⽅法调⽤者处理。
自行抛出Runtime 异常比自行抛出Checked 异常的灵活性更好。同样,抛出 Checked 异常则可以让编译器提醒程序员必须处理该异常。
throw 和 throws 区别
- 作⽤不同:throw⽤于程序员⾃⾏产⽣并抛出异常,throws⽤于声明该⽅法内抛出了异常。
- 使⽤位置不同:throw位于⽅法体内部,可以作为单独的语句使⽤;throws必须跟在⽅法参数列表的后⾯,不能单独使⽤。
- 内容不同:throw抛出⼀个异常对象,只能是⼀个;throws后⾯跟异常类,可以跟多个。
1.2.4 自定义异常
在 Java 中,我们可以通过定义自己的异常类来表示特定的错误情况和处理方式。自定义异常类通常包含以下内容:
- 定义异常类,并继承
Exception
或者RunTimeException
。 - 编写异常类的构造方法,向父类构造方法传入异常描述信息,并继承父类的其他实现方法。
- 实例化自定义异常对象,并在程序中使用
throw
抛出。
public class MyException extends Exception {
private String message;
public MyException(String message) {
this.message = message;
}
@Override
public String getMessage() {
return message;
}
}
public void myMethod(int arg) throws MyException {
if (arg < 0 || arg > 100) {
throw new MyException("Argument out of range");
} else {
// 执行正常的代码逻辑
}
}
使用自定义异常类可以为我们提供更细粒度的错误处理方式,并使代码具有更好的可读性和可维护性。
自定义异常可能是编译时异常,也可以是运行时异常。
- 如果自定义异常类继承
Excpetion
,则是编译时异常。
特点:方法中抛出的是编译时异常,必须在方法上使用throws声明,强制调用者处理。
- 如果自定义异常类继承
RuntimeException
,则运行时异常。
特点:方法中抛出的是运行时异常,不需要在方法上用throws声明。
1.2.5 异常链
在 Java 中,异常链(Exception Chaining)是指将一个异常作为另一个异常的原因而被抛出。通过异常链,我们可以更清晰地了解异常的触发原因,并且可以在捕获异常时获取到完整的异常信息。
在创建异常链时,可以使用以下两种方式之一:
-
使用带有
cause
参数的异常构造方法:异常类的构造方法中通常会包含一个带有cause
参数的重载版本,用于指定异常的原因。public MyException(String message, Throwable cause) { super(message, cause); }
在这种情况下,可以将一个异常对象作为
cause
参数传递给当前异常的构造方法。 -
使用
initCause
方法:Throwable
类提供了initCause
方法,用于将异常对象设置为另一个异常的原因。try { // ... } catch (Exception e) { MyException ex = new MyException("Custom exception"); ex.initCause(e); throw ex; }
在这种情况下,我们首先创建一个新的异常对象,然后使用
initCause
方法将之前捕获的异常对象设置为其原因。
public class TryDemoFive {
public static void main(String[] args) {
try {
testThree();
} catch (Exception e) {
// e.printStackTrace();
System.out.printf( e.getMessage());
}
}
public static void testOne() throws Exception {
throw new Exception("第一个异常");
}
public static void testTwo() throws Exception {
try {
testOne();
} catch (Exception e) {
System.out.println(e.getCause());
throw new Exception("我是新产生的异常1", e);
}
}
public static void testThree() throws Exception {
try {
testTwo();
} catch (Exception e) {
System.out.println(e.getCause());
Exception exception = new Exception("新产生的异常2");
exception.initCause(e);
throw exception;
}
}
}
null
java.lang.Exception: 第一个异常
新产生的异常2
通过使用异常链,可以在异常处理过程中保留和传递更多的信息,使开发人员能够更好地理解异常的来源和根本原因。