异常
在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:
Exception:程序本身可以处理的异常,可以通过catch来进行捕获。Exception又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。Error:Error属于程序无法处理的错误 ,可通过catch来进行捕获。但是不建议通过catch捕获, 捕获它可能掩盖系统级问题,导致程序状态不一致、资源泄漏。 。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: Error 和 Exception。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种:
- 受检异常 :需要用 try…catch… 语句捕获并进行处理,并且可以从异常中恢复;
- 非受检异常 :是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序奔溃并且无法恢复。
如果希望写一个检查性异常类,则需要继承 Exception 类。
如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。
种类
检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。
Java 内置异常类
Java 语言定义了一些异常类在 java.lang 标准包中。
异常方法
1 public String getMessage()
返回关于发生的异常的详细信息。这个消息在Throwable 类的构造函数中初始化了。
2 public Throwable getCause()
返回一个Throwable 对象代表异常原因。
3 public String toString()
使用getMessage()的结果返回类的串级名字。
4 public void printStackTrace()
打印toString()结果和栈层次到System.err,即错误输出流。
5 public StackTraceElement [] getStackTrace()
返回一个包含堆栈层次的数组。下标为0的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底。
6 public Throwable fillInStackTrace()
用当前的调用栈层次填充Throwable 对象栈层次,添加到栈层次任何先前信息中。
捕获异常
try
{
// 程序代码
}catch(ExceptionName e1)
{
//Catch 块
}
throws/throw 关键字:
如果一个方法没有捕获一个检查性异常,那么该方法必须使用 throws 关键字来声明。throws 关键字放在方法签名的尾部。
也可以使用 throw 关键字抛出一个异常,无论它是新实例化的还是刚捕获到的。
import java.io.*;
public class className
{
public void deposit(double amount) throws RemoteException
{
// Method implementation
throw new RemoteException();
}
//Remainder of class definition
}
finally关键字
finally 关键字用来创建在 try 代码块后面执行的代码块。
无论是否发生异常,finally 代码块中的代码总会被执行。
在 finally 代码块中,可以运行清理类型等收尾善后性质的语句。
finally 代码块出现在 catch 代码块最后,语法如下:
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}finally{
// 程序代码
}
声明自定义异常
在 Java 中你可以自定义异常。编写自己的异常类时需要记住下面的几点。 所有异常都必须是 Throwable 的子类。
如果希望写一个检查性异常类,则需要继承 Exception 类。
如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。
可以像下面这样定义自己的异常类:
class MyException extends Exception{
}
只继承Exception 类来创建的异常类是检查性异常类。
描述Java 7 ARM(Automatic Resource Management,自动资源管理)特征和多个catch块的使用
答:如果一个try块中有多个异常要被捕获,catch块中的代码会变丑陋的同时还要用多余的代码来记录异常。有鉴于此,Java 7的一个新特征是:一个catch子句中可以捕获多个异常。示例代码如下:
catch(IOException | SQLException | Exception ex){
logger.error(ex);
throw new MyException(ex.getMessage());
}
try-with-resources
大多数情况下,当忘记关闭资源或因资源耗尽出现运行时异常时,我们只是用finally子句来关闭资源。这些异常很难调试,我们需要深入到资源使用的每一步来确定是否已关闭。比如try里有file.readline和finally file.close都可能抛出异常,这样异常会覆盖。
因此,Java 7用try-with-resources进行了改进:在try子句中能创建一个资源对象,当程序的执行完try-catch之后,运行环境自动关闭资源。下面是这方面改进的示例代码:
class MyResource implements AutoCloseable {
MyResource() {
}
public void close() throws Exception {
System.out.println("关闭自定义资源");
}
}
try (MyResource mr = new MyResource()) {
System.out.println("MyResource created in try-with-resources");
} catch (Exception e) {
e.printStackTrace();
}
实现上,是try里自动加了close的代码在try的字节码代码块里。参考
常见错误和异常
Error
- Java 虚拟机运行错误(
Virtual MachineError) 这是一个抽象错误类,通常不会直接抛出,而是由其子类抛出,如OutOfMemoryError、StackOverflowError、InternalError等。 虚拟机内部出现了严重问题,比如内存耗尽、栈溢出、JVM 内部错误等,JVM 抛出此类错误来终止操作。 - 虚拟机内存不够错误(
OutOfMemoryError) - 类定义错误(
NoClassDefFoundError)编译时存在该类,但运行时找不到,说明编译成功后,运行时该类文件未能被 JVM 加载。- 原因
- 编译时存在该类,但运行时
.class文件不在 classpath 中 - 被删除或路径错误
- 动态加载类失败(如反射加载)
- 也可能是maven原因 依赖调解原因,它是最短路径、最早声明优先。如果 C 依赖用到了 1.5 版本的 X 中才有的一个类,运行项目就会报
NoClassDefFoundError错误。如果 C 依赖用到了 1.5 版本的 X 中才有的一个方法,运行项目就会报NoSuchMethodError错误。
- 编译时存在该类,但运行时
- 原因
- UnsupportedClassVersionError 类版本不兼容。 使用高版本 JDK 编译,低版本运行
- ExceptionInInitializerError 静态初始化器出错。 类静态代码块/字段抛异常
Exception
NullPointerException(空指针错误)IllegalArgumentException(参数错误比如方法入参类型错误)NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)ArrayIndexOutOfBoundsException(数组越界错误)ClassCastException(向下类型转换错误) 在尝试将一个对象强制转换为不是它实际类型的子类时会抛出。也就是说,编译器允许转换,但运行时发现对象类型不兼容,就会报这个错。ArithmeticException(算术错误)SecurityException(安全错误比如权限不够)UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)- ClassNotFoundException 类找不到。 使用
Class.forName时
ClassNotFoundException vs NoClassDefFoundError
| 异常/错误 | 类型 | 是否受检异常 | 触发场景 |
|---|---|---|---|
ClassNotFoundException | Exception(受检) | ✅ 是 | 通过反射等方式显式加载类时找不到,如 Class.forName(). 编译时不报错,运行时检查的 |
NoClassDefFoundError | Error(非受检) | ❌ 否 | 类在编译时存在,但运行时无法加载该类(通常因类路径问题、依赖未加载) |
两者的区别是:一个是你“主动”找类没找到(ClassNotFoundException),一个是 JVM 自动找类但没找到(NoClassDefFoundError)。
最佳实践
- try 块中的 return 语句执行成功后,并不会马上返回,而是继续执行 finally 块中的语句,如果 finally 块中也存在 return 语句,那么 try 块中的 return 就将被覆盖。注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
- 抛出具体定义的检查性异常而不是-exception
- 自定义异常时不要丢失堆栈跟踪
- finally 块中不要抛出任何异常。 如果在 finally 块中抛出异常,可能会导致原始异常被掩盖。比如说上例中,一旦 cleanup 抛出异常,someMethod 中的异常将会被覆盖。
printStackTrace()会阻塞 I/O,在高并发环境下,可能会造成性能问题。 频繁调用printStackTrace()可能会影响垃圾回收(GC),因为Throwable.fillInStackTrace()需要收集完整的堆栈信息。- 对于不打算处理的异常,直接使用 try-finally,不用 catch
- 记住早 throw 晚 catch 原则。 “早 throw, 晚 catch” 是 Java 中的一种异常处理原则。这个原则指的是在代码中尽可能早地抛出异常,以便在异常发生时能够及时地处理异常。同时,在 catch 块中尽可能晚地捕获异常,以便在捕获异常时能够获得更多的上下文信息,从而更好地处理异常。
- 切勿在代码中使用异常来进行流程控制
- error可以catch, 但不推荐。 因为它表示 JVM 运行环境的严重问题(如内存耗尽、栈溢出、类加载失败等),程序一般无法也不应该尝试恢复。