什么是异常
简单来说,就是代码出现了不正常的情况。
在 java 中,异常(Exception)是一种程序运行过程中可能发生的错误或意外事件,它会导致程序的正常执行流程中断。java 使用异常处理机制来捕获、报告和处理这些异常,保证程序的稳定性和健壮性,从而避免程序因未处理的错误而崩溃。
异常本质上是一个一个的类(Throwable)。
Throwable
在 java 中,Throwable
是所有异常和错误的根类,位于java 标准库的 java.lang
包中,Throwable
有两个直接的子类:Error
和 Exception
,它们用于描述两种不同类型的非正常情况。
Error
概念
Error
类表示程序中发生的严重错误,通常与 JVM 环境相关。- 这些错误通常是程序无法控制或恢复的,例如内存溢出、栈溢出等。
- 程序一般不需要也不应该捕获或处理这些错误。
常见子类
OutOfMemoryError
:JVM 内存不足时抛出。StackOverflowError
:递归调用过深,栈空间耗尽时抛出。VirtualMachineError
:JVM 无法继续运行时抛出,例如InternalError
或UnknownError
。
使用场景
开发者通常不需要处理 Error
,它们表明系统级的问题,需要从系统角度(如优化内存使用、调整栈大小等)解决。
Exception
概念
Exception
类及其子类用于表示程序执行过程中发生的异常情况。
这些异常通常是由程序或外部环境引发的,通常是可以通过修改代码逻辑或调整输入数据来避免或处理的。
与 Error
不同,Exception
可以进一步细分为两种情况------编译时异常和运行时异常。
编译时异常
必须在编译阶段显式处理,要么通过 try-catch
捕获并处理,要么在方法签名中使用 throws
声明由调用者负责处理。
编译时异常是我们代码写错了吗?并不是,主要是由于我们调用方法的时候,该方法底层给我们抛了一个编译时异常,所以我们一调用此方法一编译,就爆红了,我们一旦触发了这个异常,JVM 就会将异常信息打印到控制台上,给程序员们看。
Exception
类及其子类(除了 RuntimeException
)均属于编译时异常。
常见的编译时异常
IOException
:表示 I/O 操作失败,如文件读写错误。SQLException
:表示数据库访问相关的异常。
运行时异常
不需要强制性地被捕获或声明,通常是由于编程错误引起的,例如空指针引用、数组越界等。
RuntimeException
类及其子类属于运行时异常。
常见的运行时异常
NullPointerException
:尝试对一个null
对象进行方法调用或属性访问时抛出。ArrayIndexOutOfBoundsException
:访问数组元素时索引超出范围时抛出。IllegalArgumentException
:传递给方法的参数无效时抛出。
代码实例
为了更直观地理解 Error
和 Exception
的区别,以下是一个简单的例子:
java
public class ErrorAndExceptionExample {
public static void main(String[] args) {
try {
// 这里模拟一个可能导致 Error 的场景
byte[] largeArray = new byte[Integer.MAX_VALUE]; // 可能抛出 OutOfMemoryError
} catch (Error e) {
System.err.println("Caught an Error: " + e.getMessage());
}
try {
// 这里模拟一个可能导致 Exception 的场景
int[] numbers = {1, 2, 3};
System.out.println(numbers[5]); // 可能抛出 ArrayIndexOutOfBoundsException
} catch (Exception e) {
System.err.println("Caught an Exception: " + e.getMessage());
}
}
}
在这个例子中,我们首先尝试创建一个非常大的数组,这可能会导致 OutOfMemoryError
,这是一个 Error
类型的异常。然后,我们又尝试访问一个不存在的数组索引,这将抛出 ArrayIndexOutOfBoundsException
,这是一个 Exception
类型的运行时异常。注意,虽然我们可以选择捕获 Error
,但一般不建议这样做,除非你确实知道如何安全地处理这种情况。
总之,Error
和 Exception
在 java 异常处理体系中扮演着不同的角色,正确地区分和使用它们有助于编写更加健壮和可靠的程序。
异常出现的过程
什么情况下会出现异常
- 代码逻辑错误:如除以零、数组越界、空指针访问等。
- 外部资源问题:如文件不存在、网络中断、数据库连接失败等。
- 程序主动抛出异常:通过
throw
语句显式抛出。 - JVM 内部问题:如内存不足、类加载失败等。
异常触发过程
我们先来编写一段发生异常的代码(以数组越界为例)
java
public class Demo01 {
public static void main(String[] args) {
int[] array = {1,2,3,4,5};
print(array);
System.out.println("mian------我想要执行");
}
public static void print(int[] array){
System.out.println(array[6]);
System.out.println("我想要执行");
}
}
执行结果:
java
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 6
at Demo01.print(Demo01.java:9)
at Demo01.main(Demo01.java:4)
可以看到,程序在第9行(也就是执行方法 print()
中 System.out.println(array[6]);
)出现了异常,之后方法执行终止,没有输出"我想要执行",方法回到了 main
里并终止进行,也没有输出"main------我想要执行",下面详细介绍这个过程。
异常在第9行出现,出现了 ArrayIndexOutOfBoundsException
异常,JVM 认识这个异常,于是 JVM 创建了一个异常对象,这个异常对象包含异常的详细信息(如异常类型、消息、堆栈跟踪信息),然后看一看此处有没有处理这个异常,如果没有人处理,异常对象从触发点逐层向调用栈上传递,直到被捕获或到达栈顶(程序终止),我们这个程序就是到达了栈顶并且仍然没有处理,这时候就交给 JVM 自己处理,JVM 默认处理异常的方式是:
- 将异常信息打印到控制台上,也就是我们看到的:
java
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 6
at Demo01.print(Demo01.java:9)
at Demo01.main(Demo01.java:4)
- 终止程序。
异常处理的方法
在 java 中,处理异常的方式主要有以下两种方式:抛出异常(throw
)和捕获异常(try-catch
)
抛出异常(throw)
当某个方法可能抛出异常但不想在内部处理时,可以使用 throws
关键字在方法签名中声明这些异常。这样做的好处是让调用者知道可能存在哪些异常,并给予他们选择如何处理的机会。这种方式适用于那些无法预见所有异常情况或者不适合立即处理异常的场合。
抛出异常的步骤
- 在方法签名中使用
throws
声明可能抛出的异常类型。 - 在方法体内使用
throw
显式抛出异常对象。
语法结构
方法声明
java
public void method() throws ExceptionType {
// 方法体
}
抛出异常
java
throw new ExceptionType("异常信息");
示例
java
import java.io.FileNotFoundException;
public class Demo02 {
public static void main(String[] args) throws FileNotFoundException {
String str = "b.txt";
find(str);
System.out.println("main------我要执行了");
}
private static void find(String str) throws FileNotFoundException {
if (!str.equals("a.txt")){
throw new FileNotFoundException("文件找不到");
}
System.out.println("我要执行了");
}
}
结果:
java
Exception in thread "main" java.io.FileNotFoundException: 文件找不到
at Demo02.find(Demo02.java:16)
at Demo02.main(Demo02.java:8)
可以看到,这种抛出异常的方式跟 JVM 抛出异常的方法是相同,都是不作任何处理,向上抛出,直到程序终止。
补充:对于运行时异常(即继承自 RuntimeException 的异常)不需要显式声明,因为这些异常通常是由于开发人员的错误代码引起的,不是由程序运行环境的意外情况导致的。因此,Java 的设计者认为开发人员应该主动通过良好的编码习惯来避免这些问题,而不是在每个可能出现问题的地方都用显式声明来处理。
例如:
java
public class Demo02 {
public static void main(String[] args) {
int[] array = {1,2,3,4,5};
print(array,5);
System.out.println("main------我要执行了");
}
private static void print(int[] array, int index) {
if (index >= array.length){
throw new ArrayIndexOutOfBoundsException("数组越界了");
} else {
System.out.println(array[index]);
}
System.out.println("我要执行了");
}
}
可以看到不需要加 throws ArrayIndexOutOfBoundsException
程序也不会报错,而对于编译时异常(例如 FileNotFoundException
)则必须加 throws
,否则会报错。
抛出多个异常
语法结构
java
public void method() throws ExceptionType1, ExceptionType12, ...{
// 方法体
}
代码示例
java
import java.io.FileNotFoundException;
import java.io.IOException;
public class Demo02 {
public static void main(String[] args) throws FileNotFoundException, IOException {
String str = "b.txt";
find(str);
}
private static void find(String str) throws FileNotFoundException, IOException {
if (!str.equals("a.txt")){
throw new FileNotFoundException("文件找不到");
} else if (str == null){
throw new IOException("文件不能为空");
}
}
}
补充:如果 throws
的多个异常之间存在子父类继承关系,我们可以直接 throws
父类异常,例如上述代码中 FileNotFoundException
是 IOException
的子类,我们就可以直接:
java
import java.io.FileNotFoundException;
import java.io.IOException;
public class Demo02 {
public static void main(String[] args) throws IOException {
String str = "b.txt";
find(str);
}
private static void find(String str) throws IOException {
if (!str.equals("a.txt")){
throw new FileNotFoundException("文件找不到");
} else if (str == null){
throw new IOException("文件不能为空");
}
}
}
如果不知道这些异常之间是否存在子父类继承关系,可以直接 throws Exception
,Exception
是所有异常类的父类
捕获异常(try-catch)
这是最常见的一种异常处理方式,它允许你在程序中直接处理可能出现的异常,而不需要将异常传递给调用者。使用 try-catch
结构可以确保即使发生异常,程序也能继续正常运行,并且你可以对异常进行适当的处理。
语法
java
try {
可能出现异常的代码
} catch (ExceptionType 异常对象名){
处理异常代码
}
示例
java
import java.io.FileNotFoundException;
public class Demo03 {
public static void main(String[] args){
try {
String str = "b.txt";
find(str);
System.out.println("try------我要执行了");
} catch (FileNotFoundException e){
System.out.println(e);
System.out.println(e.getMessage());
};
System.out.println("main------我要执行了");
}
private static void find(String str) throws FileNotFoundException {
if (!str.equals("a.txt")){
throw new FileNotFoundException("文件找不到");
}
System.out.println("我要执行了");
}
}
结果:
java
java.io.FileNotFoundException: 文件找不到
文件找不到
main------我要执行了
可以看到,使用这种方法捕获异常后,try
代码块内终止运行,没有输出"try------我要执行了",但是程序没有终止,输出了"main------我要执行了"。
捕获多个异常
语法
java
try {
可能出现异常的代码
} catch (ExceptionType1 异常对象名){
处理异常代码1
} catch (ExceptionType2 异常对象名){
处理异常代码2
}
可用于处理捕获多个异常。
注意:如果多个异常之间存在子父类关系,需要把子类放在上面先去 catch,因为一旦 catch 到异常,后续的 catch 都不会去执行
finally 关键字
需要配合 try-catch
块去使用,可加可不加,无论是否发生了异常,finally
块中的代码都会被执行。通常用来释放资源,如关闭文件流或网络连接等。
特殊情况:如果之前使用 System.exit(0)
终止了当前正在执行的 java 虚拟机,则不会执行。
语法
java
try {
可能出现异常的代码
} catch (ExceptionType 异常对象名){
处理异常代码
} finally {
不管是否发生异常都会执行的代码块
}
示例
java
import java.io.FileNotFoundException;
public class Demo04 {
public static void main(String[] args){
try {
String str = "b.txt";
find(str);
System.out.println("try------我要执行了");
} catch (FileNotFoundException e){
System.out.println(e);
System.out.println(e.getMessage());
} finally {
System.out.println("finally------我要执行了");
};
System.out.println("main------我要执行了");
}
private static void find(String str) throws FileNotFoundException {
if (!str.equals("a.txt")){
throw new FileNotFoundException("文件找不到");
}
System.out.println("我要执行了");
}
}
结果:
java
java.io.FileNotFoundException: 文件找不到
文件找不到
finally------我要执行了
main------我要执行了
无论异常有没有发生,finally 代码块的内容都会被执行
注意事项
对于子类重写父类的方法情况:
如果父类方法抛出了异常,那么子类重写方法可抛可不抛。
如果父类方法没有抛出异常,子类重写方法也不能抛出异常。
自定义异常
在 java 中,自定义异常是指由开发者根据应用程序的具体需求创建的异常类。通过定义自己的异常类型,你可以为特定的业务逻辑错误提供更加精确和有意义的反馈信息,这有助于提高代码的可读性和维护性。自定义异常通常是通过继承现有的异常类来实现的,比如 Exception
或者它的子类(对于编译时异常),以及 RuntimeException
(对于运行时异常)。
使用自定义异常的原因
- 业务场景的特殊性:
- 系统内置异常不能完全描述业务逻辑中可能出现的问题,例如,在一个银行系统中,"账户余额不足"是一个非常具体的错误场景,使用
IllegalArgumentException
或其他通用异常并不能很好地传达这一信息。 - 通过自定义异常,可以更清晰地表达问题来源和意义。
- 增强代码的可读性和可维护性:
- 自定义异常的名称可以直接反映问题的具体含义,使开发和调试更方便。
- 统一异常处理:
- 在大型项目中,通过自定义异常可以统一管理异常逻辑,便于集中处理和记录日志。
- 促进模块化设计:
- 当不同的模块或组件之间需要交换错误信息时,自定义异常可以帮助保持接口的一致性和清晰度。
创建自定义异常类
需求,判断用户是否存在,创建一个 LoginUserException
。
LoginUserException
类:
java
public class LoginUserException extends Exception {
public LoginUserException() {
}
public LoginUserException(String message) {
super(message);
}
}
main:
java
import java.util.Scanner;
public class Demo05 {
public static void main(String[] args) {
// 输入用户名,判断用户是否存在
Scanner scanner = new Scanner(System.in);
String user = scanner.next();
try {
login(user);
} catch (LoginUserException e) {
}
}
private static void login(String user) throws LoginUserException {
// 创建一个存在的用户
String username = "root";
if (user.equals(username)) {
System.out.println("登录成功");
} else {
throw new LoginUserException("用户不存在");
}
}
}
打印异常信息的方式
打印方式主要有3种,都是在 Throwable
类中
java
String toString() 输出异常类型和设置的异常信息。
String getMessage() 输出设置的异常信息。
void printStackTrace() 打印异常信息最全的:包括异常类型,信息,异常出现的行数等。
在 catch 中添加代码:
java
System.out.println(e.toString());
System.out.println(e.getMessage());
e.printStackTrace();
结果:
java
LoginUserException: 用户不存在
用户不存在
LoginUserException: 用户不存在
at Demo05.login(Demo05.java:26)
at Demo05.main(Demo05.java:11)
自定义异常信息
企业中自定义异常主要包括两个属性,编码(code)和异常信息(message) ,我们可以通过更改编码和异常信息来适用不同的异常(例如用户名错误和密码错误),重写方法来输出这些信息。
示例
LoginUserException
:
java
public class LoginUserException extends Exception {
int code;
String msg;
public LoginUserException(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
@Override
public String toString() {
return code + ":" + msg;
}
}
main:
java
import java.util.Scanner;
public class Demo05 {
public static void main(String[] args) {
// 输出用户名和密码,判断用户是否存在
Scanner scanner = new Scanner(System.in);
String user = scanner.next();
String password = scanner.next();
try {
login(user, password);
} catch (LoginUserException e) {
System.out.println(e.toString());
}
}
private static void login(String username,String password) throws LoginUserException {
// 创建一个存在的用户
String user = "root";
String psd = "123456";
if (username.equals(user) && password.equals(psd)) {
System.out.println("登录成功");
} else if (!username.equals(user)){
throw new LoginUserException(1001,"用户名错误");
} else if (!password.equals(psd)){
throw new LoginUserException(1002,"密码错误");
}
}
}
结果:
java
user
123456
1001:用户名错误
java
root
111111
1002:密码错误
自定义异常最佳实践
- 选择合适的父类 :如果你的异常是程序逻辑的一部分并且应该被显式地处理,那么继承
Exception
是合理的;否则,如果异常是由于编程错误引起的,则可以考虑继承RuntimeException
。 - 提供详细的错误信息:确保你的异常类能够携带足够的上下文信息,以便于调试和解决问题。除了消息文本之外,还可以包括额外的数据字段,如错误代码等。
- 遵循命名约定 :为了保持一致性,建议将异常类命名为以
Exception
结尾,并且名称应清楚地反映出异常所代表的情况。 - 不要过度使用自定义异常:虽然自定义异常有很多好处,但也不要为每一个可能的错误都创建新的异常类型。只有当标准异常不足以描述问题时,才应该引入自定义异常。
通过合理地创建和使用自定义异常,你可以使你的应用程序更加健壮、易于理解和维护。