本篇博客着重讲解运行时异常和检查型异常。
目录
为什么会设计异常处理机制?
设计异常处理机制的原因主要基于以下几个方面的考虑:
-
程序的健壮性:异常处理机制可以确保程序在遇到错误或异常情况时不会立即崩溃,而是能够以一种可控的方式处理这些问题。通过捕获和处理异常,程序可以优雅地恢复或至少提供一个清晰的错误消息给用户,而不是直接终止执行。
-
错误管理:异常处理机制提供了一种结构化的方式来管理程序中可能出现的错误。通过将错误处理代码与正常业务逻辑分离,可以使得代码更加清晰、易于阅读和维护。同时,异常处理机制也允许程序员定义自定义的异常类型,以更好地适应特定应用或库的需求。
-
错误传播:在Java中,异常可以在方法之间传播。当一个方法无法处理某个错误时,它可以通过抛出异常将错误传递给调用它的方法。这种机制允许错误在程序的不同层次之间传播,直到找到能够处理它的地方。这种错误传播的方式使得错误处理更加灵活和可扩展。
是什么?
异常处理机制是Java编程语言中的一个重要特性,它允许程序在运行时处理错误情况,而不是立即终止。这种机制提供了一种结构化的方式来处理可能出现的错误,从而使程序更加健壮和可靠。
异常类
Throwable
Throwable是Java语言中所有错误(Error)和异常(Exception)的超类。
说明:在Java中,每一个异常都是一个异常类,它们都是Throwable类的直接或间接子类。Throwable类有两个重要的子类:Error和Exception,它们分别代表了不同类型的异常情况。
Error
通常表示JVM遇到了严重的问题,这些问题可能是配置错误、资源不足或底层系统问题导致的。
说明:描述Java虚拟机无法解决的严重错误,通常无法编码解决,如JVM挂掉了等。
异常类 | 描述 | 意义 |
---|---|---|
Error | 所有错误的超类。 | 表示一个严重的错误,通常不应该由应用程序代码来捕获或处理。 |
AssertionError | 当断言失败时抛出。 | 用于开发和测试期间验证程序状态的正确性。 |
ClassCircularityError | 当类定义直接或间接地包含自己时抛出。 | 表示类定义中存在循环引用,这是不允许的。 |
ClassFormatError | 当尝试加载的类文件不符合Java虚拟机规范时抛出。 | 表示类文件已损坏或格式不正确。 |
InternalError | 表示Java虚拟机内部错误或资源耗尽。 | 指示JVM本身遇到了问题,通常需要进一步调查。 |
LinkageError | 指示一个类依赖于另一个类,而后者在运行时不可用。 | 是与类链接相关的错误的超类。 |
NoClassDefFoundError | 当应用程序试图加载一个类,而找不到定义时抛出。 | 指示类路径配置错误或类文件丢失。 |
NoSuchFieldError | 当应用程序试图访问或修改一个不存在的字段时抛出。 | 表示类定义中不存在所引用的字段。 |
NoSuchMethodError | 当应用程序试图调用一个不存在的方法时抛出。 | 表示类定义中不存在所引用的方法。 |
OutOfMemoryError | 当Java虚拟机没有足够的内存来完成请求时抛出。 | 指示JVM堆内存耗尽,需要调整堆大小或优化内存使用。 |
StackOverflowError | 当应用程序递归调用过深导致调用栈溢出时抛出。 | 指示递归调用没有正确的退出条件或方法调用链过长。 |
ThreadDeath | 当线程调用其stop() 方法时抛出。 | 虽然是一个Error ,但实际上是一个线程退出通知,不一定会导致线程停止。 |
UnknownError | 指示一个未知的错误。 | 表示发生了未分类的错误,需要进一步调查。 |
UnsatisfiedLinkError | 当Java虚拟机无法找到指定的本地库时抛出。 | 指示JNI调用所需的本地库不可用或版本不兼容。 |
VerifyError | 当验证器检测到类文件字节码中的错误时抛出。 | 表示类文件在字节码验证阶段失败,可能由于类文件损坏或使用了不安全的操作。 |
Exception
Exception类是所有异常类的基类,并且进一步被分为两大类:运行时异常(RuntimeException)和检查型异常(Checked Exception)。
说明:主要用于描述因编程错误或偶然外在因素导致的轻微错误。
运行时异常(RuntimeException)
在编写代码时,编译器不会强制你捕获这种异常,但可以选择性的捕获或抛出。
异常意义:表示不可恢复的异常,如程序bug和致命错误。
异常类 | 功能描述 |
---|---|
RuntimeException | 所有运行时异常的基类。这些异常在Java程序中通常不需要显式捕获处理,因为它们通常指示编程错误或不可恢复的情况。 |
NullPointerException | 当应用程序试图在需要对象的地方使用null 时抛出。 |
ArrayIndexOutOfBoundsException | 当应用程序试图访问数组的非法索引时抛出。 |
ClassCastException | 当试图将对象强制转换为不是实例的子类时抛出。 |
NumberFormatException | 当应用程序试图将一个字符串转换成一种数值类型,但该字符串不能转换为适当格式时抛出。 |
ConcurrentModificationException | 当一个线程尝试修改一个对象,而同时另一个线程正在迭代该对象的集合时抛出。 |
IllegalArgumentException | 当向方法传递非法或不适当参数时抛出。 |
IllegalStateException | 当在对象的状态不允许操作的情况下调用方法时抛出。 |
示例:
//NullPointerException(空指针异常)
String str = null;
System.out.println(str.length()); // 尝试访问null对象的length属性,将抛出NullPointerException
//ArrayIndexOutOfBoundsException(下标越界异常)
int[] array = new int[5];
System.out.println(array[10]); // 尝试访问数组的第10个元素,将抛出ArrayIndexOutOfBoundsException
//ArithmeticException(计算异常)
int result = 10 / 0; // 尝试除以零,将抛出ArithmeticException
System.out.println(result);
//ClassCastException(类型转换异常)
Object obj = new Integer(5);
String str = (String) obj; // 尝试将Integer对象转换为String,将抛出ClassCastException
System.out.println(str);
//IllegalArgumentException
public static void main(String[] args) {
methodWithArgument("invalid"); // 假设该方法不接受"invalid"作为参数
}
public static void methodWithArgument(String arg) {
if ("invalid".equals(arg)) {
throw new IllegalArgumentException("Invalid argument provided");
}
// ... 正常逻辑处理 ...
}
//ConcurrentModificationException
ArrayList<String> list = new ArrayList<>();
list.add("item1");
list.add("item2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.equals("item1")) {
list.remove(item); // 在迭代过程中修改集合,将抛出ConcurrentModificationException
}
}
检查型异常(Checked Exception)
在编译代码时,编译器会强制让你捕获或抛出这种异常,并在编译时显示处理。
异常意义:表示可能导致程序状态不确定或破坏程序健壮性的异常情况。
异常类 | 功能描述 |
---|---|
Exception(除RuntimeException及其子类外的所有异常) | 所有检查型异常的基类。这些异常在编译时必须被显式捕获处理,否则编译器会报错。 |
IOException | 当发生输入/输出错误时抛出。这是文件操作和网络编程中最常见的异常类型。 |
ClassNotFoundException | 当应用程序试图加载类,而找不到指定的类时抛出。 |
SQLException | 提供关于数据库访问错误的信息。当尝试连接到数据库失败,或执行SQL语句失败时抛出。 |
InterruptedException | 当一个线程正在等待、睡眠或占用时,另一个线程中断它时抛出。 |
ParseException | 在解析过程中发生的异常,例如在解析日期或文本格式时。 |
FileNotFoundException | 当试图打开指定路径名不存在的文件时抛出。这是IOException的子类。 |
示例:
//IOException(IO流异常)
File file = new File("nonexistent.txt");
try (FileInputStream fis = new FileInputStream(file)) {
// 尝试打开不存在的文件将抛出FileNotFoundException,它是IOException的子类
byte[] buffer = new byte[1024];
int bytesRead = fis.read(buffer);
// ... 处理读取到的数据 ...
} catch (IOException e) {
// 捕获并处理IOException
System.err.println("读取文件时发生错误: " + e.getMessage());
}
//ClassNotFoundException(找不到指定类异常)
try {
Class.forName("com.example.NonExistentClass"); // 尝试加载不存在的类
} catch (ClassNotFoundException e) {
// 捕获并处理ClassNotFoundException
System.err.println("找不到指定的类: " + e.getMessage());
}
//SQLException(JDBC操作数据库异常)
Connection conn = null;
Statement stmt = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/nonexistentdb", "user", "password");
stmt = conn.createStatement();
stmt.execute("SELECT * FROM some_table"); // 尝试在不存在的数据库中执行查询
} catch (SQLException e) {
// 捕获并处理SQLException
System.err.println("数据库操作失败: " + e.getMessage());
} finally {
// 关闭资源
try {
if (stmt != null) stmt.close();
if (conn != null) conn.close();
} catch (SQLException ex) {
// 处理关闭资源时的异常
ex.printStackTrace();
}
}
//FileNotFoundException(打开文件失败异常)
File file = new File("nonexistent.txt");
try (FileInputStream fis = new FileInputStream(file)) {
// 尝试打开不存在的文件将抛出FileNotFoundException
} catch (FileNotFoundException e) {
// 捕获并处理FileNotFoundException
System.err.println("找不到文件: " + e.getMessage());
}
//InterruptedException(线程中断异常)
Thread thread = new Thread(() -> { // 创建一个新线程
try {
// 让线程睡眠一段时间
Thread.sleep(5000);
} catch (InterruptedException e) {
// 处理InterruptedException
System.out.println("Thread was interrupted: " + e.getMessage());
// 通常,在捕获到InterruptedException后,我们应该重新设置中断状态
Thread.currentThread().interrupt();
}
});
// 启动线程
thread.start();
// 主线程等待一秒后中断子线程
Thread.sleep(1000);
thread.interrupt();
//ParseException(解析异常)
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String dateString = "invalid-date-string";
try {
// 尝试解析日期字符串
Date date = dateFormat.parse(dateString);
System.out.println("Parsed date: " + date);
} catch (ParseException e) {
// 处理ParseException
System.out.println("Parse error: " + e.getMessage());
}
自定义异常
为什么需要自定义异常?
当Java标准库中的异常类无法准确描述你的程序可能遇到的特定错误情况时,你可以创建自定义异常来提供更精确的错误信息。
自定义异常如何选择异常父类
当创建一个自定义异常时,你应该根据你的异常是否应该被强制处理来决定是继承 RuntimeException 还是其他 Exception 子类。如果你希望你的异常是检查型异常(即,需要显式处理),那么你应该直接继承 Exception 或其非 RuntimeException 的子类。如果你希望你的异常是运行时异常(即,不需要显式处理),那么你应该继承 RuntimeException。
如何自定义异常?
自定义异常通常继承自Exception或RuntimeException,可以定义带参或无参的构造函数,用于初始化异常对象。
示例:
// 带错误消息的构造函数
public class MyCustomException extends Exception {
public MyCustomException(String message) {
super(message); // 调用父类Exception的构造函数
}
// 可以根据需要添加其他构造函数或方法
}
public class CustomExceptionExample {
public static void main(String[] args) throws MyCustomException {
try {
throw new MyCustomException("这是一个自定义异常"); //设置异常
} catch (MyCustomException e) {
System.out.println("捕获到自定义异常: " + e.getMessage());
}
}
}
相关疑问
异常有优先级的问题吗?
异常并没有所谓的“优先级”概念。
当程序产生多个异常时,异常捕获的处理逻辑是依据代码中的try-catch块来确定,会按照catch块的顺序依次从上到下尝试匹配并处理异常。
当异常被抛出时,Java会检查每个catch块,并找到第一个能够处理该异常类型的catch块。一旦找到匹配的catch块,异常就被处理,后续的catch块将不会被检查。
多个异常同时发生如何捕获和处理?
当程序产生多个异常时,通常一次只会处理一个异常。如果一个try块中的代码抛出了多个异常,那么只有第一个被抛出的异常会被处理。后续的异常只有在第一个异常被处理完毕并且程序继续执行到可能再次抛出异常的地方时,才有可能被抛出和处理。
自定义异常为什么需要调用父类Exception的构造方法?
创建一个自定义异常的类时,调用父类Exception的构造方法是为了初始化异常对象的状态,特别是异常消息(message)。
异常消息是一个字符串,用于描述异常发生的原因或上下文。这对于理解异常发生的原因以及如何进行调试或错误处理非常有帮助。
PS:当创建一个自定义异常对象时,通常希望提供一个描述性的消息来解释异常发生的原因。如果你不调用父类的构造方法,并且自定义异常类中没有定义自己的方式来存储异常消息,那么你的自定义异常类将不会包含任何关于异常原因的描述信息。这通常不是我们所期望的,因为异常消息是异常处理中非常重要的一个部分。
总的来说,调用父类Exception的构造方法是为了确保自定义异常能够正确地存储和传递异常消息,从而提供更丰富的上下文信息来帮助理解和处理异常。
示例:
public class CustomException extends Exception {
public CustomException(String message) {
super(message); // 调用父类Exception的构造方法,初始化异常消息
}
}
运行时异常和检查型异常不同点?
- 可预见性与恢复性:检查型异常通常代表那些可以预见并且可能通过适当的错误处理来恢复的异常情况。这些异常通常与外部环境或系统资源有关,如文件操作、网络连接等。运行时异常则更多地表示程序错误,如逻辑错误、空指针引用或数组越界等。这些异常通常是程序内部的问题,通常难以预见,且一旦发生,往往表示程序处于不稳定状态,难以恢复。
- 处理策略:运行时异常在编译时不需要在代码中显式捕获,但也可以显式的捕获或抛出。检查型异常必须在编译时捕获或抛出,否则编译器会报错。
- 关注点:运行时异常主要关注的是程序运行时的错误和问题。检查型异常则更多地关注那些需要在编译时进行检查和处理的异常情况。
- 继承关系:在Java中,所有的异常类都是Throwable类的子类。检查型异常是Exception类及其非RuntimeException子类的所有子类。而运行时异常则是RuntimeException类及其子类的所有异常。
运行时异常为什么不设计成必须捕获或抛出?
不设计运行时异常为必须捕获或抛出的原因有以下几点:
- 运行时异常的性质:运行时异常通常表示程序错误,如逻辑错误、空指针引用或数组越界等。这些异常是程序内部的问题,通常难以预见,且一旦发生,往往表示程序处于不稳定状态,难以恢复。因此,强制捕获这些异常可能会增加代码的复杂性,而并不能有效地解决问题。
- 分离正常代码与错误处理代码:如果要求捕获所有运行时异常,那么代码中将会充斥大量的try-catch块,这会导致正常代码与错误处理代码的混杂,降低代码的可读性和可维护性。通过将运行时异常设计为无需捕获,Java允许程序员将错误处理代码与正常代码分离,使代码结构更加清晰。
- 保证程序的健壮性:虽然运行时异常不需要强制捕获,但程序员仍然可以选择捕获并处理这些异常。这样,程序员可以根据具体需求来决定是否对某个特定的运行时异常进行处理,从而确保程序的健壮性。
- 避免编程繁琐性:检查型异常在编译时必须进行显式处理,这在一定程度上增加了编程的繁琐性。如果运行时异常也要求强制捕获,那么将会进一步增加编程的复杂性。通过将运行时异常设计为无需捕获,Java简化了异常处理机制,使程序员能够更专注于实现业务逻辑。
为什么不建议捕获运行时异常?
不建议捕获运行时异常主要受运行时异常的性质、避免代码冗余和复杂性、鼓励更好的错误处理策略,并帮助程序员区分可预见且可能恢复的异常与程序错误导致的异常。
捕获和处理异常
语法
基本语法
说明:try-catch必须同时存在,是异常捕获和处理的基本结构。
try {
// 尝试执行的代码块,可能会抛出异常
// 这里是可能产生异常的代码
} catch (ExceptionType e) {
// 当try块中抛出ExceptionType类型的异常时,执行这里的代码
// 处理ExceptionType类型的异常
}
其它写法
写法一:finally块是一个可选的,finally块中的代码总是会被执行。
try {
// 尝试执行的代码块,可能会抛出异常
// 这里是可能产生异常的代码
} catch (ExceptionType e) {
// 当try块中抛出ExceptionType类型的异常时,执行这里的代码
// 处理ExceptionType类型的异常
}finally {
// 不论是否发生异常,finally块中的代码都会执行
// 常用于释放资源、关闭文件等操作
}
写法二:允许多重catch捕获不同的异常。
try {
// 尝试执行的代码块,可能会抛出异常
// 这里是可能产生异常的代码
} catch (ExceptionType e) {
// 当try块中抛出ExceptionType类型的异常时,执行这里的代码
// 处理ExceptionType类型的异常
} catch (ExceptionType2 e) {
// 当try块中抛出ExceptionType2类型的异常时,执行这里的代码
// 处理ExceptionType2类型的异常
// 可以继续添加更多的catch块来处理不同类型的异常
}
写法三:允许一个catch块捕获多种异常。
try {
// 可能抛出异常的代码
} catch (ExceptionType e | ExceptionType2 e2) {
// 处理ExceptionType或ExceptionType2
}
PS:try-catch是必须有的,其它写法可根据需求任意组合。
从概念层面捕理解获和处理
异常的捕获和处理是紧密相关的,通常是通过try-catch语句块来实现的。不过,从概念上,我们可以将“捕获”和“处理”分开来解释,以帮助理解异常处理的流程。(实际代码方面:捕获和处理是不可分开的)
以下通过概念的方式分类:(实际不可分开!!!)
捕获异常:
捕获异常指的是使用try块来包围可能会抛出异常的代码。当这些代码执行时,如果发生了异常,Java虚拟机会查找相应的catch块来处理这个异常。如果没有找到合适的catch块,那么这个异常会继续向上传播,直到被更高层的代码捕获或者最终由Java虚拟机处理(通常会导致程序终止)。
处理异常:
处理异常是指在catch块中编写代码来应对捕获到的异常。处理异常的方式可以是记录错误信息、尝试恢复程序的正常执行流程、或者执行一些清理操作。catch块中的代码应该针对捕获到的特定类型的异常进行处理,确保程序能够在发生异常时以合理的方式继续运行或终止。
示例:
try {
// 尝试执行可能会抛出异常的代码
int result = 10 / 0; // 这会抛出ArithmeticException
} catch (ArithmeticException e) {
// 处理捕获到的ArithmeticException异常
System.out.println("捕获到了除数为零的异常: " + e.getMessage());
// 可以在这里执行更多的处理逻辑
}
相关疑问
除了Error类和子类之外的所有的异常都可以被捕获吗?
是的,除了Error类及其子类之外的所有异常在技术上都可以被捕获,但是否应该捕获某个特定的异常取决于具体的业务需求和异常处理策略。
错误(Error)可以被捕获吗?
Error类及其子类表示严重的问题,如虚拟机错误或系统崩溃。这些错误通常是无法恢复的,因此即使使用try-catch块也无法捕获和处理它们。在大多数情况下,当Error发生时,程序将终止运行。
PS:无法通过异常处理来恢复的异常。对于这些情况,更好的策略是预防它们的发生,而不是尝试在运行时捕获和处理它们。
异常抛出
throw
在方法内部显式地抛出一个异常对象。(一定抛出一个异常)
语法:后面必须跟随一个新创建的异常对象或一个已经存在的异常对象。
public void someMethod() {
try {
// some code that might throw an exception
throw new Exception("An error occurred");
} catch (Exception e) {
// handle the exception
}
}
throws
在方法签名中声明该方法可能会抛出的异常。(可能抛出一个异常)
语法:在方法签名中声明该方法可能会抛出的异常类型。
public void doSomething() throws MyException {
// ... 可能会抛出MyException的代码 ...
}
相关疑问
throw和throws的区别?
相同点:都是将异常抛给调用者处理。
不同点:抛出异常位置不同。throw在方法内部显式地抛出异常,方法签名中声明该方法可能会抛出的异常。
抛出异常可以一直抛出不处理吗?
检查型异常不能一直被抛出而不处理;它们必须在方法签名中声明,并且最终需要被捕获和处理。这是Java强制类型检查和异常处理机制的一部分,旨在确保程序在运行时能够更健壮地处理异常情况。
运行时异常允许你抛出异常而不处理,但这通常不是一个好的做法。
throw和throws必须联用吗?
不需要,在一个方法中只使用throws来声明可能抛出的异常,而不实际抛出任何异常。如果你在方法内部使用throw抛出了一个检查异常(checked exception),那么该方法必须使用throws声明这个异常,除非该异常在当前方法内部被捕获并处理。
// 检查型异常,必须在方法签名上声明
public void checkedExceptionMethod() throws IOException {
throw new IOException("Checked exception");
}
// 非检查型异常,不需要在方法签名上声明
public void uncheckedExceptionMethod() {
throw new RuntimeException("Unchecked exception");
}