一、Java 中常见的异常
二、Error:程序的 “绝症”
在 Java 的异常体系中,Error 类有着特殊且关键的地位。它通常是由 Java 虚拟机(JVM)抛出,代表着那些非常严重的、程序自身往往难以控制和恢复的问题。
比如说最常见的 OutOfMemoryError,当 JVM 不再有足够的内存资源来继续执行操作时,就会抛出这个异常。想象一下,程序在运行过程中不断地申请内存空间,可内存就像一个已经装满水的杯子,再也装不下了,这时候就触发了这个 “内存耗尽” 的严重状况。还有 StackOverflowError,一般在进行深度递归调用,导致栈空间被耗尽时出现。例如下面这个简单的递归函数示例,若没有合适的终止条件,就很容易引发栈溢出异常:
public class StackOverflowExample {
public static void recursiveFunction() {
recursiveFunction();
}
public static void main(String[] args) {
recursiveFunction();
}
}
这些由 Error 类表示的错误,大多和代码编写者所执行的常规操作关系不大,它们更多是在虚拟机自身运行或者执行应用时出现故障导致的。而且从实际处理角度来看,对于设计合理的应用程序而言,一旦这类错误发生了,本质上也不应该试图去处理它所引起的异常状况,因为其超出了程序本身的控制和处理能力范围。通常情况下,遇到 Error 类的异常,对应的线程一般就会终止运行了。
三、Exception:尚能补救的问题
Exception 类表示的是程序本身可以处理的异常情况,它和 Error 有着明显的区别,是我们在日常编程中经常需要关注并处理的部分。Exception 又可以大致分为两大类,即编译时异常和运行时异常。
编译时异常,从程序语法角度讲是必须进行处理的异常,如果不处理,程序在编译阶段就无法通过。像 IOException(比如进行文件读写操作时,文件不存在或者没有相应权限等情况会抛出该异常)、SQLException(在数据库操作中,如连接数据库失败、执行 SQL 语句出错等场景下抛出)等都属于这一类。例如,以下代码尝试读取一个不存在的文件时,就会抛出 IOException,如果不使用 try-catch 捕获或者在方法声明处用 throws 声明该异常,编译就会报错:
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class CompileTimeExceptionExample {
public static void main(String[] args) {
File file = new File("nonexistent.txt");
try {
FileReader reader = new FileReader(file);
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行时异常,是 RuntimeException 及其子类异常,常见的如 NullPointerException(当应用程序试图访问空对象时抛出)
public class NullPointerExceptionExample {
public static void main(String[] args) {
String string = null;
System.out.println(string.toString());
}
}
ArrayIndexOutOfBoundsException(访问数组超出其范围的元素时出现)
public class NullPointerExceptionExample {
public static void main(String[] args) {
int[] arr = {1, 2};
System.out.println(arr[2]);
}
}
这类异常的特点是 Java 编译器不会强制要求对其进行处理,即使程序中可能出现这类异常,没有用 try-catch 语句捕获它,也没有用 throws 子句声明抛出它,代码也能编译通过。不过,虽然编译器不强制,但我们从程序逻辑角度出发,还是应该尽可能避免这类异常的发生,因为它们通常是由程序逻辑错误引起的。
总之,在编写 Java 程序时,要对 Exception 类的这些不同情况心里有数,合理地运用异常处理机制,来增强程序的健壮性、可读性以及维护性,确保程序在面对各种可能出现的异常情况时,都能稳定运行或者做出合适的应对。
四、异常处理三板斧
try-catch 语句:捕获异常
在 Java 中,try-catch 语句是异常处理的常用方式之一。其基本结构如下:
try {
// 可能会抛出异常的代码
} catch (异常类型1 变量名1) {
// 处理异常类型1的代码
} catch (异常类型2 变量名2) {
// 处理异常类型2的代码
}
// 可以有多个catch块,依次类推
我们会将那些可能出现异常的代码放置在 try 块当中。例如,当进行文件读取操作时,像下面这样的代码就可能产生异常:
try {
FileReader reader = new FileReader("test.txt");
// 后续对文件读取的相关操作
} catch (IOException e) {
// 在这里处理文件读取出现的IO异常,比如输出错误信息等
System.out.println("文件读取出现异常:" + e.getMessage());
}
当 try 块中的代码一旦抛出异常,程序就会立即跳转到与之匹配的 catch 块中进行处理。并且可以设置多个 catch 块来处理不同类型的异常,不过要注意,如果 catch 中的异常类型存在子父类关系,那么要求子类异常类型的 catch 块一定要声明在父类异常类型的 catch 块上面,否则编译会报错。例如:
try {
int[] arr = {1, 2};
System.out.println(arr[3]); // 会抛出数组越界异常
int num = 10 / 0; // 会抛出算术异常
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数组越界异常被捕获:" + e.getMessage());
} catch (ArithmeticException e) {
System.out.println("算术异常被捕获:" + e.getMessage());
} catch (Exception e) {
// Exception作为父类异常放在最后,兜底捕获其他未处理的异常
System.out.println("其他异常被捕获:" + e.getMessage());
}
通过这样的 try-catch 结构,我们能够精准地捕获各种可能出现的异常情况,让程序在遇到异常时不会轻易崩溃,而是按照我们预设的方式进行处理,进而增强程序的健壮性。
finally 子句:兜底保障
finally 子句是 Java 异常处理机制中一个非常重要的部分,无论 try 块中的代码是否抛出异常,finally 块中的代码都一定会被执行。其常见的语法形式如下:
try {
// 可能出现异常的代码
} catch (异常类型 变量名) {
// 处理异常的代码
} finally {
// 无论如何都会执行的代码
}
它常用于释放一些程序所占用的资源,比如关闭文件、数据库连接等操作。例如,在文件读取完毕后,无论读取过程中是否出现异常,都需要关闭文件流,代码示例如下:
InputStream in = null;
try {
in = new FileInputStream("example.txt");
// 进行文件读取相关操作
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in!= null) {
try {
in.close(); // 关闭文件输入流,确保资源释放
} catch (IOException e) {
e.printStackTrace();
}
}
}
还有在数据库连接操作场景中,使用完连接后需要关闭连接以释放资源,像这样:
Connection connection = null;
try {
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "username", "password");
// 执行数据库相关操作,如查询、更新等
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (connection!= null) {
try {
connection.close(); // 关闭数据库连接
} catch (SQLException e) {
e.printStackTrace();
}
}
}
需要注意的是,当 finally 子句中包含 return 语句时,会有一些特殊情况出现。假设在 try 语句块中有 return 语句准备返回一个值,但是在真正返回前,finally 子句的内容将会被执行,如果 finally 子句中也有一个 return 语句,那么这个 return 语句的值将会覆盖原始 try 语句块中准备返回的值,所以在编写代码时要格外小心这种情况,避免出现不符合预期的结果。
throws 关键字:异常转移
throws 关键字通常用在方法声明的后面,用于告知调用者该方法可能会抛出某些异常,将异常抛给调用者去处理。其语法格式为:
修饰符 返回值类型 方法名(参数列表) throws 异常类型1, 异常类型2,... {
// 方法体
}
比如,有一个读取文件内容的方法,在读取过程中可能会出现IOException异常,就可以这样声明:
public void readFile(String fileName) throws IOException {
FileReader reader = new FileReader(fileName);
// 其他文件读取操作
}
使用 throws 关键字的场景往往是当方法内部出现了异常,但是该方法本身不适合或者不方便处理这个异常时,就选择将异常抛出去,让调用这个方法的上层代码去决定如何处理。
它和 throw 关键字是有区别的,throw 是用于在方法内部抛出一个异常实例,例如:
public void checkValue(int value) {
if (value < 0) {
throw new IllegalArgumentException("输入的值不能为负数");
}
}
而 throws 只是在方法声明处声明可能抛出的异常类型,并不实际抛出异常对象。
在实际开发中,如果一个方法中调用了其他可能抛出异常的方法,并且自身不想处理这些异常,那么可以使用 throws 关键字将异常继续向上传递,直到有合适的地方去处理这些异常,比如在 Java 的分层架构中,底层的数据访问层方法可能会将数据库操作相关的异常(如SQLException)通过 throws 抛给业务逻辑层,业务逻辑层再根据情况决定是继续抛给上层的控制层,还是在本层进行 try-catch 处理,以此来合理地管理和处理异常,保障程序的正常运行。
五、异常处理的 “九阳真经”
精准捕获
在异常处理中,精准捕获异常至关重要。我们应尽量精确地捕获特定类型的异常,避免宽泛的异常捕获。例如,在处理文件读取操作时,FileNotFoundException是针对文件不存在的异常,而IOException则涵盖了更广泛的文件操作异常。如果我们能够明确地捕获FileNotFoundException,就能更好地处理文件不存在的情况,而不是用IOException来兜底。这样可以让程序在遇到异常时,更加准确地定位问题所在。
合理抛转
当方法内部出现异常且自身无法处理时,应合理地抛出异常。例如,在数据库操作中,如果出现SQLException,可以将其抛出给调用者,让调用者来处理。但要注意,在抛出异常时,应确保异常信息的准确性和完整性,以便调用者能够正确地理解和处理异常。
记录日志
记录日志是异常处理的重要环节。通过记录异常信息,我们可以了解异常发生的时间、地点、异常类型以及相关的错误信息。在 Java 中,常用的日志框架有 Log4j、Logback 和 SLF4J 等。例如,在try-catch块中,使用logger.error方法记录异常信息,这样可以方便开发人员在调试和排查问题时,快速定位异常。
避免空 catch
在编写catch块时,应避免空 catch。空 catch 会使程序在遇到异常时无法进行有效的处理,可能导致隐藏的问题无法及时发现。例如,在捕获IOException时,应至少打印出异常信息,以便了解异常发生的原因。
巧用 finally
finally 子句无论是否发生异常都会执行。在 finally 块中,可以进行一些资源清理操作,如关闭文件、数据库连接等。例如,在文件读取操作完成后,无论是否发生异常,都可以在 finally 块中关闭文件流,确保资源的正确释放。
自定义异常
自定义异常可以更好地满足业务需求。例如,在一个学生成绩管理系统中,学生成绩不能为负数,否则抛出NegativeScoreException。通过自定义异常,可以使程序的逻辑更加清晰,同时也便于维护和扩展。
总之,通过掌握这些异常处理技巧,我们可以优化代码,提高程序的健壮性和可靠性。在实际开发中,应根据具体情况灵活运用这些技巧,使程序在遇到异常时能够更好地处理和应对。