异常
什么是异常
在编程中,异常(Exception)是指程序运行过程中发生的不正常或非预期的行为,它可能由编程错误、硬件故障、用户操作不当或其他外部因素引起。异常处理是程序设计中非常重要的一部分,它允许程序在遇到错误时,能够优雅地处理错误情况,而不是直接崩溃或产生不可预测的行为。
异常的主要特点:
-
运行时错误:异常通常表示程序在运行时遇到了错误。
-
非预期行为:异常的发生通常是不可预测的,它们可能由外部因素或程序内部逻辑错误引起。
-
影响程序流程:异常的发生会中断正常的程序流程。
-
需要处理:为了程序的健壮性,异常需要被适当地捕获和处理。
异常的分类:
在Java中,异常分为两大类:
-
编译时异常(Checked Exceptions):
- 这些异常需要在编译时被捕获或声明抛出。
- 它们通常是可预见的,比如
IOException
或SQLException
。 - 编译时异常是
Exception
类的子类,但不包括RuntimeException
类及其子类。
-
运行时异常(Runtime Exceptions):
- 这些异常不需要强制捕获或声明抛出,它们通常是编程错误导致的,如
NullPointerException
或ArrayIndexOutOfBoundsException
。 - 运行时异常是
RuntimeException
类的子类。
- 这些异常不需要强制捕获或声明抛出,它们通常是编程错误导致的,如
-
错误(Errors):
- 错误是程序无法处理的严重问题,如
OutOfMemoryError
或StackOverflowError
。 - 它们通常是由于系统问题或资源限制引起的,不是正常程序逻辑能够处理的。
- 错误是程序无法处理的严重问题,如
异常处理机制:
Java使用异常处理机制来处理程序中的错误情况,主要包含以下几个关键字:
try
:尝试执行的代码块,可能会抛出异常。catch
:捕获try
块中抛出的异常,并进行处理。finally
:无论是否发生异常,都会执行的代码块,通常用于资源清理。throw
:在代码中手动抛出一个异常。throws
:声明方法可能抛出的异常,调用者需要处理这些异常。
示例代码:
public class ExceptionExample {
public static void main(String[] args) {
try {
// 尝试执行的代码
int result = 10 / 0; // 这里会发生算术异常
} catch (ArithmeticException e) {
// 捕获并处理异常
System.out.println("发生算术异常:" + e.getMessage());
} finally {
// 无论是否发生异常都会执行
System.out.println("这是 finally 块,用于资源清理等。");
}
}
}
代码中,try
块中的除以零操作会抛出 ArithmeticException
异常,然后在 catch
块中被捕获并处理。finally
块中的代码无论是否发生异常都会执行。
Java异常类结构图
Error类与Exception类的区别
在Java中,Error
和 Exception
都是 Throwable
类的子类,但它们之间存在一些关键的区别:
-
严重性:
Error
表示编译时无法检测到的错误,通常是严重的、不可避免的问题,如OutOfMemoryError
、StackOverflowError
等。这些问题通常与代码逻辑无关,而是与JVM或系统资源有关。Exception
表示可以在编译时检测到的异常情况,通常是由于编程错误或外部因素引起的问题,如IOException
、NullPointerException
等。
-
可控性:
Error
通常不需要程序去捕获和处理,因为它们是不可恢复的,程序遇到这类错误通常会终止。Exception
是可以被捕获并处理的,通过异常处理机制(try
、catch
、finally
、throws
),程序可以在遇到异常时进行一些清理工作或提供备用方案。
-
分类:
Error
是java.lang.Error
类的实例,通常用来指示不应该被应用程序捕获的严重问题。Exception
是java.lang.Exception
类的实例,进一步分为两大类:- 编译时异常(Checked Exceptions):需要在编译时被捕获或声明抛出。
- 运行时异常(Runtime Exceptions):不需要强制捕获或声明抛出,通常是由编程错误引起的。
-
处理方式:
- 对于
Error
,通常的做法是不去捕获它们,而是让程序终止,或者在最高级别的catch
块中捕获并记录日志,因为它们通常指示了严重的问题。 - 对于
Exception
,建议通过异常处理机制来捕获并适当处理,以提高程序的健壮性和用户体验。
- 对于
-
使用场景:
Error
类型通常用于指示JVM的内部错误,如资源耗尽、配置错误等。Exception
类型用于指示程序运行过程中可能遇到的问题,如文件找不到、网络连接失败等。
-
自定义异常:
- 开发者可以创建自定义的
Exception
类型来表示特定的异常情况。 - 开发者通常不会创建自定义的
Error
类型,因为它们表示了不应该被处理的严重问题。
- 开发者可以创建自定义的
Exception分类
运行时异常类
运行时异常(Runtime Exceptions)是Java中 RuntimeException
类的子类,它们表示在程序运行时可能发生的错误情况。与编译时异常(Checked Exceptions)不同,运行时异常不需要在代码中显式声明抛出(使用 throws
关键字),也不需要被捕获处理。
运行时异常的主要特点:
-
编程错误:运行时异常通常是由于编程错误导致的,比如逻辑错误或不正确的输入数据。
-
无需声明:Java编译器不强制要求方法声明抛出运行时异常。
-
自动恢复:运行时异常通常不需要程序自动恢复,因为它们往往指示了严重的问题,程序可能无法继续正常运行。
-
非检查性:运行时异常是非检查性异常,意味着方法调用者可以选择捕获处理它们,也可以选择忽略。
-
常见类型:
NullPointerException
:尝试使用了一个未被初始化(null)的对象时抛出。IllegalArgumentException
:当一个方法接收到非法或不合适的参数时抛出。IllegalStateException
:当一个对象的状态不满足请求的操作时抛出。IndexOutOfBoundsException
:访问数组或字符串的非法索引时抛出。ConcurrentModificationException
:在迭代过程中修改了集合时抛出。ArithmeticException
:算术运算出错时抛出,如除以零。NumberFormatException
:尝试将字符串转换为数字,但字符串不是适当的格式时抛出。
示例代码:
public class RuntimeExceptionExample {
public static void main(String[] args) {
int[] numbers = {1, 2, 3};
try {
// 这将抛出 ArrayIndexOutOfBoundsException
System.out.println(numbers[4]);
} catch (RuntimeException e) {
System.out.println("捕获到运行时异常: " + e.getMessage());
}
}
public static void processNumber(int number) {
if (number < 0) {
throw new IllegalArgumentException("不接受负数");
}
// 处理数字的代码
}
}
示例中,尝试访问数组 numbers
的索引 4
将抛出 ArrayIndexOutOfBoundsException
,这是一个运行时异常。在 processNumber
方法中,如果传入的 number
是负数,方法将抛出 IllegalArgumentException
。
Java中的运行时异常(RuntimeException)类及其常见的子类包括但不限于以下这些:
RuntimeException
- 所有运行时异常的超类。NullPointerException
- 当应用程序尝试使用null
的对象引用进行操作时抛出。IllegalArgumentException
- 当一个方法接收到不正确或不合适的参数时抛出。IllegalStateException
- 当一个对象的状态不满足某种请求时抛出。IndexOutOfBoundsException
- 包括ArrayIndexOutOfBoundsException
和StringIndexOutOfBoundsException
,当访问数组或字符串的非法索引时抛出。ConcurrentModificationException
- 当一个迭代器的底层集合被修改时抛出,而迭代器没有预期到这种修改。ArithmeticException
- 包括OverflowException
和UnderflowException
,当发生算术错误时,如除以零或整数溢出。NumberFormatException
- 当尝试将字符串转换为数字,但该字符串不符合数字格式时抛出。ArrayStoreException
- 当尝试将一个不兼容类型的元素放入一个对象数组时抛出。ClassCastException
- 当尝试将一个对象强制转换为不兼容的类型时抛出。UnsupportedOperationException
- 当不支持请求的操作时抛出。BufferOverflowException
- 当试图向一个已经满了的缓冲区写入数据时抛出。BufferUnderflowException
- 当试图从空的缓冲区读取数据时抛出。ClosedCollectorsException
- 当尝试从已关闭的收集器中检索流时抛出。DuplicateFormatFlagsException
- 当格式字符串包含重复的格式标志时抛出。EOFException
- 当输入流到达文件结束(EOF)并且还有更多数据需要读取时抛出。EmptyStackException
- 当从空栈中弹出或检索元素时抛出。ExceptionInInitializerError
- 当静态初始化器导致异常时抛出。IllegalAccessError
- 当尝试访问不可访问的字段时抛出。IncompatibleClassChangeError
- 当尝试将类加载到不兼容的类定义时抛出。InstantiationError
- 当尝试使用new
关键字实例化一个抽象类或接口时抛出。
受检查异常类(非运行时)
受检查异常类(Checked Exceptions)是Java中需要在编译时通过 throws
关键字声明或通过 try-catch
语句捕获处理的异常。这些异常通常是可预见的,并且可以被程序处理或恢复。受检查异常是 Exception
类的子类,但不包括 RuntimeException
类及其子类。
以下是一些常见的受检查异常类:
IOException
- 输入/输出异常,如文件读写错误。SQLException
- 数据库访问异常。FileNotFoundException
- 文件未找到异常。MalformedURLException
- URL格式错误异常。IllegalAccessException
- 非法访问控制异常,如尝试访问不可见的字段。InstantiationException
- 类实例化异常,如尝试实例化一个抽象类或接口。ClassNotFoundException
- 类未找到异常,通常在加载类时发生。InterruptedException
- 中断异常,如线程在等待、休眠或接受输入时被中断。UnsupportedEncodingException
- 字符编码不支持异常。ParserConfigurationException
- 解析器配置异常,通常与XML解析有关。SAXException
- SAX解析异常,通常与XML解析有关。DOMException
- 文档对象模型(DOM)异常,通常与XML或HTML文档操作有关。IllegalArgumentException
- 非法参数异常,尽管它也是RuntimeException
的子类,但通常在需要强制类型转换或处理不合规参数时使用。NumberFormatException
- 数字格式异常,如尝试将一个非数字字符串转换为数字。TimeoutException
- 超时异常,如操作在指定时间内未完成。
受检查异常的声明和处理是Java强制执行的,目的是让开发者在编写代码时就考虑到可能发生的异常情况,并做出相应的处理。这有助于提高程序的健壮性和可靠性。
示例代码:
public class CheckedExceptionExample {
public static void main(String[] args) {
try {
readFile("nonexistentfile.txt");
} catch (IOException e) {
System.out.println("发生IO异常: " + e.getMessage());
}
}
public static void readFile(String fileName) throws IOException {
// 假设这里是文件读取逻辑
if (fileName == null || fileName.isEmpty()) {
throw new FileNotFoundException("文件名为空或文件不存在");
}
// 文件处理代码...
}
}
示例中,readFile
方法声明抛出 IOException
,调用者必须捕获这个异常或进一步声明抛出。这演示了受检查异常的用法。
异常处理机制
抛出异常
在Java中,抛出异常(Throwing an Exception)是指在代码执行过程中遇到某种错误或不期望发生的情况时,使用 throw
关键字来手动中断正常的程序流程,并传递一个异常对象给调用者或异常处理机制。
以下是抛出异常的一些关键点:
-
使用
throw
关键字:在Java中,使用throw
关键字后跟一个异常对象来抛出异常。 -
抛出异常对象:可以抛出Java API中预定义的异常类型,也可以抛出自定义的异常类型。
-
方法声明中的
throws
关键字:如果方法内部抛出了受检查异常(checked exceptions),则需要在方法签名中使用throws
关键字声明这些异常。 -
异常处理:抛出的异常需要被调用者捕获并处理,或者继续被调用者声明抛出。
-
资源清理:在抛出异常之前,应当确保已经进行了必要的资源清理工作,比如关闭文件流或网络连接。
-
异常链:在捕获一个异常并抛出另一个异常时,可以通过构造函数或
initCause()
方法将原始异常作为原因传递给新的异常对象,这称为异常链。
示例代码:
public class ExceptionThrowingExample {
public static void main(String[] args) {
try {
performAction();
} catch (Exception e) {
System.out.println("捕获到异常: " + e.getMessage());
}
}
public static void performAction() throws Exception {
if (someCondition()) {
// 抛出一个受检查异常
throw new Exception("发生了一个受检查异常");
} else {
// 抛出一个运行时异常
throw new RuntimeException("发生了一个运行时异常");
}
}
private static boolean someCondition() {
// 条件逻辑
return true; // 假设总是返回 true
}
}
在上面的示例中,performAction
方法根据某个条件抛出不同类型的异常。main
方法中的 try-catch
块捕获并处理了这些异常。
throw与throws的用法
在Java中,throw
和 throws
是异常处理机制的两个关键概念,它们在处理异常时扮演不同的角色:
throw
关键字的用法:
throw
关键字用于在代码中手动抛出一个异常。当某个条件满足时,你可以使用 throw
来抛出一个具体的异常对象。这通常发生在你想要中断当前方法的执行并通知方法的调用者处理这个异常时。
throw
后面通常跟着一个异常对象实例。throw
可以抛出任何类型的Throwable
对象,包括Exception
和Error
。- 使用
throw
可以在任何代码块中抛出异常,包括方法体、循环、条件语句等。
示例代码:
public void checkAge(int age) {
if (age < 18) {
throw new IllegalArgumentException("年龄必须至少为18岁");
}
// 年龄合法的其他逻辑...
}
throws
关键字的用法:
throws
关键字用于在方法签名中声明该方法可能会抛出的异常。当你的方法没有处理某个异常,而是想让调用者来处理时,你需要在方法签名中使用 throws
声明这个异常。
throws
后面跟着的是可能会被抛出的异常类型列表。- 使用
throws
声明的异常需要被方法的调用者捕获或进一步声明抛出。 throws
只能用于方法签名,不能用于其他位置。
示例代码:
public void processFile() throws IOException {
// 如果文件操作失败,可能会抛出IOException
// 例如,尝试打开一个不存在的文件
FileReader fileReader = new FileReader("nonexistentfile.txt");
// 文件读取逻辑...
}
在这个例子中,processFile
方法可能会抛出 IOException
,因此在方法签名中声明了 throws IOException
。这意味着调用 processFile
方法的代码需要处理这个异常,或者在它的声明中进一步声明抛出 IOException
。
总结:
throw
用于在代码中实际抛出一个异常。throws
用于在方法签名中声明该方法可能会抛出的异常,让调用者知道需要处理这些异常。
捕获异常
在Java中,异常的处理顺序遵循特定的规则,确保异常能够被正确地捕获和处理。以下是异常处理的顺序和相关概念:
-
异常的抛出:
- 使用
throw
关键字在代码中抛出一个异常。
- 使用
-
寻找最近的
catch
块:- 一旦异常被抛出,Java虚拟机(JVM)会从抛出点开始,向上级调用栈中寻找匹配的
catch
块。
- 一旦异常被抛出,Java虚拟机(JVM)会从抛出点开始,向上级调用栈中寻找匹配的
-
匹配异常类型:
catch
块会按照代码中出现的顺序进行匹配。最先匹配的catch
块(异常类型匹配或异常类型是catch
块中声明的异常类型的子类)会被执行。
-
执行
catch
块:- 找到匹配的
catch
块后,执行其中的代码来处理异常。
- 找到匹配的
-
继续执行后续代码:
- 一旦一个
catch
块执行完成,程序会继续执行catch
块之后的代码,除非异常处理过程中发生了返回或抛出了新的异常。
- 一旦一个
-
finally
块的执行:- 如果存在
finally
块,无论是否捕获并处理了异常,finally
块中的代码都会执行。这通常用于执行清理工作,如关闭文件流或释放资源。
- 如果存在
-
方法返回或异常传递:
- 如果异常被捕获并处理,方法可能会正常返回或通过
throw
抛出新的异常。 - 如果没有找到匹配的
catch
块,异常会沿着调用栈向上传递,直到被处理或导致程序终止。
- 如果异常被捕获并处理,方法可能会正常返回或通过
-
调用栈展开:
- 在寻找
catch
块或异常被传递的过程中,调用栈会展开,即逐层退出已经进入但未完成的方法。
- 在寻找
-
异常传播:
- 如果方法通过
throws
声明了异常,调用者需要决定是捕获异常还是继续传递。
- 如果方法通过
示例代码:
public class ExceptionHandlingOrder {
public static void main(String[] args) {
try {
mightThrowException();
} catch (IOException e) {
System.out.println("捕获 IOException: " + e.getMessage());
} catch (Exception e) {
System.out.println("捕获其他类型的 Exception: " + e.getMessage());
} finally {
System.out.println("这是 finally 块,无论是否捕获异常都会执行。");
}
}
public static void mightThrowException() throws IOException {
try {
// 某些操作,可能会抛出 IOException 或其他类型的 Exception
throw new IOException("I/O 操作失败");
} catch (IOException e) {
// 捕获 IOException 并重新抛出
throw e;
} finally {
// 清理资源
System.out.println("mightThrowException 方法的 finally 块");
}
}
}
在这个示例中,mightThrowException
方法可能会抛出 IOException
。在 main
方法中,有两个 catch
块分别捕获 IOException
和其他类型的 Exception
。无论是否捕获异常,main
方法的 finally
块都会执行。如果在 mightThrowException
方法中没有捕获异常,它将被传递到 main
方法中,并由相应的 catch
块处理。
异常传播
异常传播(Exception Propagation)指的是当一个方法内部抛出异常后,该异常可以被其调用者捕获和处理,如果调用者没有处理,异常可以继续向上传递到更高级别的调用者,直到被捕获或传递到应用程序的最顶层。以下是异常传播的几个关键点:
-
方法内部抛出异常:
- 在方法内部使用
throw
抛出一个异常。
- 在方法内部使用
-
调用者捕获异常:
- 如果方法的调用者提供了
try-catch
块,它可以直接捕获并处理该异常。
- 如果方法的调用者提供了
-
异常声明抛出:
- 如果方法的调用者没有捕获异常,它可以在其方法签名中使用
throws
关键字声明抛出该异常。
- 如果方法的调用者没有捕获异常,它可以在其方法签名中使用
-
继续向上传递:
- 如果调用者没有捕获也没有声明抛出异常,异常将继续向上传递到调用者的调用者,这个过程会一直持续到异常被捕获或传递到应用程序的最顶层。
-
顶层异常处理:
- 如果异常最终没有被捕获,它将到达Java运行时环境,可能会显示错误信息或采取默认的异常处理措施。
-
异常链:
- 在捕获一个异常并抛出另一个异常时,可以使用异常链(通过构造函数或
initCause()
方法)来保留原始异常的信息。
- 在捕获一个异常并抛出另一个异常时,可以使用异常链(通过构造函数或
-
运行时异常的传播:
- 运行时异常(RuntimeException)不需要声明抛出,它们可以自由地在调用栈中传播,直到被捕获或导致程序终止。
-
受检查异常的传播:
- 受检查异常(Checked Exceptions)必须在方法中被捕获或通过
throws
关键字声明抛出。
- 受检查异常(Checked Exceptions)必须在方法中被捕获或通过
-
异常传播的终止:
- 异常传播可以在任何地方被终止,只要有一个
catch
块捕获了异常。
- 异常传播可以在任何地方被终止,只要有一个
-
应用程序的健壮性:
- 异常传播机制允许程序在遇到错误时,提供错误信息,并且可以中断或跳过一些代码的执行,以防止程序继续执行可能出错的操作。
示例代码:
public class ExceptionPropagationExample {
public static void main(String[] args) {
try {
methodA();
} catch (Exception e) {
System.out.println("在main方法中捕获异常: " + e.getMessage());
}
}
public static void methodA() throws Exception {
try {
methodB();
} catch (IOException e) {
System.out.println("在methodA中捕获IOException: " + e.getMessage());
// 可以选择在这里重新抛出异常,或者处理它
throw e; // 重新抛出异常,继续异常传播
}
}
public static void methodB() throws IOException {
// 某些操作导致IOException
throw new IOException("发生I/O异常");
}
}
在这个示例中,methodB
抛出了一个 IOException
。methodA
捕获了这个异常,并打印了信息,然后选择重新抛出异常,继续异常传播。main
方法捕获了从 methodA
传递来的异常,并打印了信息。如果 main
方法没有捕获这个异常,它将导致程序终止。
自定义异常
自定义异常是指开发者根据特定需求创建的异常类。这些类通常继承自Java的 Exception
类或其子类,用以表示特定的错误情况或业务逻辑中的问题。自定义异常使得异常处理更加具体化和细粒度化。
创建自定义异常的步骤:
-
确定异常类型:
- 决定自定义异常是编译时异常(
Exception
的子类,除了RuntimeException
)还是运行时异常(RuntimeException
的子类)。
- 决定自定义异常是编译时异常(
-
继承异常类:
- 创建一个新的类,继承自
Exception
或RuntimeException
。
- 创建一个新的类,继承自
-
添加构造函数:
- 为自定义异常类提供构造函数,至少包括一个无参构造函数和一个接受错误消息字符串的构造函数。通常还会提供一个接受错误原因(
Throwable
类型)的构造函数。
- 为自定义异常类提供构造函数,至少包括一个无参构造函数和一个接受错误消息字符串的构造函数。通常还会提供一个接受错误原因(
-
定义异常信息:
- 可以添加额外的属性和方法来提供更多关于异常的信息。
-
使用异常链:
- 使用
initCause()
方法或在构造函数中传递原始异常,来创建异常链,这有助于调试。
- 使用
-
实现
Serializable
接口:- 确保自定义异常类实现了
Serializable
接口,这样异常对象可以被序列化和传递。
- 确保自定义异常类实现了
示例代码:
// 自定义编译时异常
public class CustomException extends Exception {
public CustomException() {
super();
}
public CustomException(String message) {
super(message);
}
public CustomException(String message, Throwable cause) {
super(message, cause);
}
public CustomException(Throwable cause) {
super(cause);
}
}
// 自定义运行时异常
public class CustomRuntimeException extends RuntimeException {
public CustomRuntimeException() {
super();
}
public CustomRuntimeException(String message) {
super(message);
}
public CustomRuntimeException(String message, Throwable cause) {
super(message, cause);
}
public CustomRuntimeException(Throwable cause) {
super(cause);
}
}
// 使用自定义异常
public class CustomExceptionExample {
public static void checkCondition(boolean condition) throws CustomException {
if (!condition) {
throw new CustomException("条件不满足,抛出自定义异常");
}
}
public static void main(String[] args) {
try {
checkCondition(false);
} catch (CustomException e) {
System.out.println("捕获自定义异常: " + e.getMessage());
}
}
}
在上面的示例中,CustomException
是一个自定义的编译时异常,而 CustomRuntimeException
是一个自定义的运行时异常。checkCondition
方法在条件不满足时抛出 CustomException
。main
方法中使用 try-catch
块来捕获并处理这个自定义异常。