Java异常是Java编程语言中用于表示程序运行时错误的一种机制。Java异常体系通过异常类和异常处理来实现,允许程序在遇到预期或意外情况时,优雅地处理问题,而不是立即终止程序运行。
异常类层次结构
Java异常类都继承自java.lang.Throwable
类,它是所有异常和错误的根类。Throwable
类有两个直接子类:Error
和Exception
。
- Error:这是程序无法捕获或恢复的严重错误,如系统崩溃、内存溢出等。这类错误通常不需要程序处理,因为它们通常是不可控的系统级错误。
- Exception :这是程序
在运行过程中
可能出现的可以捕获和处理的异常情况。Exception又分为两类:- Checked Exception(编译时异常) :编译器要求必须显式处理的异常,如果不处理,代码无法编译通过。例如:
IOException
、SQLException
等。 - Unchecked Exception(运行时异常) :也被称为
RuntimeException
,编译器不要求必须捕获这类异常,但是如果出现则会导致程序立即停止运行,除非它们在代码中被捕获。例如:NullPointerException
、IllegalArgumentException
、ArrayIndexOutOfBoundsException
等。
- Checked Exception(编译时异常) :编译器要求必须显式处理的异常,如果不处理,代码无法编译通过。例如:
编译时异常
编译时异常,也称为受检查异常
,是指在编译阶段就需要处理的异常。这类异常通常由程序外部环境或非程序自身逻辑错误引起,比如I/O错误
、网络通信失败
、数据库连接失败
等。编译时异常强调的是异常的预见性和可控性,要求程序员在编写代码时就必须考虑如何处理这些异常。
特点:
- 当方法可能抛出编译时异常时,必须在方法签名中通过
throws
关键字声明该异常,或者在方法体内部使用try-catch
块捕获并处理异常。 - 如果调用含有声明编译时异常的方法的地方没有处理这个异常,编译器会提示错误,直到异常被适当地捕获或声明抛出为止。
- 常见的编译时异常包括:
IOException
、SQLException
、ClassNotFoundException
等。
运行时异常
运行时异常,也称为未检查异常
,是指在编译时不强制处理的异常,它们通常由程序内部逻辑错误导致,如空指针异常
、数组越界
、算术异常
等。运行时异常强调的是程序运行时的正确性和完整性,它们通常反映出代码逻辑的缺陷,程序员也应该尽量避免这些异常的发生,但编译器并不会强制处理。
特点:
- 编译器不会强迫程序员在方法签名中声明运行时异常,也不会因为在方法体内没有处理运行时异常而导致编译失败。
- 程序员可以选择捕获并处理运行时异常,但这不是必需的。若没有捕获,当运行时异常发生时,程序将终止,栈轨迹(StackTrace)会显示异常的发生位置和相关信息。
- JVM默认会抛出运行时异常,除非在调用栈中的某一层有适当的处理代码。
- 常见的运行时异常包括:
NullPointerException
、ArrayIndexOutOfBoundsException
、ClassCastException
、IllegalArgumentException
、ArithmeticException
等。
JVM默认处理异常的方式
当Java程序
在运行时抛出一个异常,并且没有在当前线程的调用栈中找到合适的catch块来捕获这个异常时,JVM会按照以下步骤来处理这个未捕获的异常:
- 寻找最近的未处理异常处理器(UncaughtExceptionHandler) :
- 每个线程都可以设置一个未处理异常处理器,如果线程抛出了未捕获的异常,JVM会首先查找该线程是否设置了自定义的
Thread.UncaughtExceptionHandler
,如果有,则调用该处理器的uncaughtException(Thread t, Throwable e)
方法来处理异常。
- 每个线程都可以设置一个未处理异常处理器,如果线程抛出了未捕获的异常,JVM会首先查找该线程是否设置了自定义的
- 默认的未处理异常处理器 :
- 如果线程没有设置自定义的
UncaughtExceptionHandler
,JVM将使用默认的异常处理器。默认的处理器通常会打印异常的堆栈跟踪信息到标准错误输出(System.err)。
- 如果线程没有设置自定义的
- 终止线程 :
- 无论是自定义的还是默认的未处理异常处理器,在处理完异常后,JVM通常会选择终止抛出异常的线程。对于主线程(main thread)而言,整个Java应用也会随之退出,因为它承载了Java程序的入口点。
说明:
JVM对于未捕获的异常,默认行为是打印堆栈追踪信息到标准错误输出,并结束抛出异常的线程(如果是主线程,则结束整个Java应用程序)。当然,开发人员可以通过设置自定义的UncaughtExceptionHandler
来改变这种默认行为,以便进行更细致的异常处理,比如记录日志、通知监控系统或者进行其他必要的清理工作。
异常处理机制
Java提供了以下几种机制来处理异常:
throws关键字
- 在方法签名中声明该方法可能抛出的异常。
throws
格式是跟在方法的括号后面的
定义格式:
java
public void 方法() throws 异常类名1,异常类名2 {
}
代码示例:
java
public class Demo01_Throws {
public static void main(String[] args) throws ParseException {
System.out.println("开始");
// method();
method2();
System.out.println("结束");
}
// 运行时异常
public static void method() throws ArrayIndexOutOfBoundsException{
int[] arr = {1, 2, 3};
System.out.println(arr[3]);
}
// 编译时异常
public static void method2() throws ParseException {
String s = "2048-08-09";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date d = sdf.parse(s);
System.out.println(d);
}
}
throw关键字
- 使用
throw
关键字可以抛出一个异常对象,后面的代码不再执行。 - 格式:
throw new 异常();
代码示例:
java
public class Demo02_Throw {
public static void main(String[] args) {
int [] arr = {1,2,3,4,5};
// int [] arr = null;
printArr(arr);//就会 接收到一个异常.
//我们还需要自己处理一下异常.
}
private static void printArr(int[] arr) {
if(arr == null){
//调用者知道成功打印了吗?
//System.out.println("参数不能为null");
throw new NullPointerException(); //当参数为null的时候
//手动创建了一个异常对象,抛给了调用者,产生了一个异常
}else{
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
}
try-catch语句
- 用于捕获异常并提供恢复措施。
执行流程:
- 程序从
try
里面的代码开始执行 - 出现异常,就会跳转到对应的
catch
里面去执行 - 执行完毕之后,程序还可以继续往下执行
java
try {
// 可能抛出异常的代码
} catch (ExceptionType name) {
// 异常处理代码
}
try-catch-finally语句
- 无论是否发生异常,都会执行的代码块。
java
try {
// 可能抛出异常的代码
} catch (ExceptionType name) {
// 异常处理代码
} finally {
// 总会执行的代码
}
代码示例:
java
public class Demo03_Try {
public static void main(String[] args) {
System.out.println("开始");
method();
System.out.println("结束");
}
public static void method() {
try {
int[] arr = {1, 2, 3};
System.out.println(arr[3]);
System.out.println("这里能够访问到吗");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("你访问的数组索引不存在,请回去修改为正确的索引");
}finally {
System.out.println("一直会执行,一般用来释放资源...");
}
}
}
try-with-resources语句
try-with-resources
语句是一种用于自动管理和关闭资源的异常处理机制,它从Java 7开始引入,旨在简化资源清理工作,确保即使在发生异常的情况下,资源也能被正确地关闭。这种语句适用于那些实现了java.lang.AutoCloseable
接口的对象,如文件流、套接字、数据库连接等,这些对象在使用完毕后需要显式关闭以释放系统资源。
基本语法:
java
try (
ResourceType resource1 = initializer1;
ResourceType resource2 = initializer2;
// ... 其他资源声明与初始化
) {
// 在此处使用资源进行操作
}
catch (ExceptionType1 ex1) {
// 处理与resource1或resource2等相关的异常
}
catch (ExceptionType2 ex2) {
// 处理其他特定类型的异常
}
finally {
// 可选的finally块,用于执行额外的清理工作(非资源关闭)
}
特点与优势:
- 自动关闭 :当
try
块结束时,不论是因为正常执行到结束还是因为抛出并捕获了异常,Java都会自动调用资源对象的close()
方法来关闭资源。这避免了手动编写finally
块来确保资源关闭,提高了代码的简洁性和可靠性。 - 多资源支持 :
try
语句内的资源声明可以有多个,用分号隔开。所有资源按照声明顺序逆序关闭,即后声明的资源先关闭。这样即使在关闭一个资源时抛出异常,后续资源仍有机会被正确关闭。 - 异常处理 :如果有异常在
try
块内抛出,catch
子句可以捕获并处理这些异常。如果在关闭资源过程中也抛出了异常,那么这个关闭异常会被抑制(suppressed)并添加到已存在的异常中(可通过Throwable.getSuppressed()
访问)。如果try
块内没有异常,但关闭资源时抛出异常,则这个关闭异常会作为try-with-resources
语句的结果抛出。
代码示例:
使用java.util.Scanner
读取用户从控制台输入的一行文本:
java
try (Scanner scanner = new Scanner(System.in)) {
System.out.println("请输入文本: ");
String inputLine = scanner.nextLine();
System.out.println("你输入的文本: " + inputLine);
} catch (Exception e) {
System.err.println("读取录入内容出错:");
e.printStackTrace();
}
自定义异常
开发者可以创建自定义异常类,通常是通过继承Exception
类或其子类,用来表示程序中特有的、标准异常类无法精确描述的异常情况。
创建自定义异常类的步骤:
- 继承异常基类 :自定义异常类通常继承自
java.lang.Exception
或其子类。如果希望自定义的是运行时异常(无需强制捕获),可以继承自java.lang.RuntimeException
。如果希望自定义的是编译时异常(需要强制捕获),则直接继承自java.lang.Exception
。 - 添加构造方法:自定义异常类通常至少包含一个构造方法,用于初始化异常对象。构造方法通常接受一个字符串参数,用于存储详细的异常信息(如错误描述)。还可以添加其他构造方法,如接受多个参数或无参构造方法。
- 可选地,添加属性和方法:根据需要,自定义异常类可以添加特定的属性(如错误代码、错误详情等)和方法,以便提供更多关于异常的上下文信息。
代码示例
创建一个简单的自定义异常类示例:
java
public class InvalidInputException extends Exception {
public InvalidInputException(String message) {
super(message);
}
public InvalidInputException(String message, Throwable cause) {
super(message, cause);
}
}
解析:
InvalidInputException
继承自java.lang.Exception
,意味着它是一个编译时异常,需要在代码中显式捕获或声明抛出。- 定义了两个构造方法:
- 第一个构造方法接收一个字符串参数
message
,用来描述异常的具体信息。它调用父类Exception
的构造方法,将传入的message
传递给父类,以便在异常堆栈信息中显示。 - 第二个构造方法接收两个参数:
message
和cause
。message
同上,cause
是一个Throwable
对象,用于表示引发此异常的底层原因。这个构造方法同样调用父类的相应构造方法,将message
和cause
传递给父类。
- 第一个构造方法接收一个字符串参数
使用自定义异常类的示例:
假设有一个方法calculateAverage()
,它接收一个整数数组并计算平均值。如果数组为空,我们希望抛出InvalidInputException
:
java
public class Demo05_ExcetionTest {
public static void main(String[] args) throws InvalidInputException {
int[] num = {99,65,78,63,45,15,94,64};
double ca = calculateAverage(num);
System.out.println(ca);//输出: 65.375
}
/* 获取数组的平均值*/
public static double calculateAverage(int[] numbers) throws InvalidInputException {
if (numbers == null || numbers.length == 0) {
throw new InvalidInputException("Input array is empty or null.");
}
int sum = 0;
for (int number : numbers) {
sum += number;
}
return (double) sum / numbers.length;
}
}
解析:
- 方法
calculateAverage()
声明抛出InvalidInputException
,表明它可能在执行过程中抛出这个自定义异常。 - 如果传入的数组为空或为
null
,方法会使用InvalidInputException
的构造方法创建一个新的异常对象,并通过throw
关键字抛出该异常。
异常处理的最佳实践
- 捕获具体的异常 :避免捕获
Throwable
或Exception
类,这会隐藏错误和程序中的问题。 - 提供有用的异常信息:在自定义异常中提供有用的错误信息。
- 避免在finally中抛出异常 :
finally
块中抛出的异常会覆盖之前捕获的异常,导致调试困难。 - 使用受检异常表示非程序逻辑错误:如文件不存在、用户名无效等。
- 使用非受检异常表示程序逻辑错误:如空指针访问、数组越界等。