1、异常类结构
Exception 位于 java.lang 包下,它是一种顶级接口,继承于 Throwable 类,Exception 类及其子类都是 Throwable 的组成条件,是程序出现的合理情况。
Java 程序经常会出现两种问题,一种是 java.lang.Exception ,一种是 java.lang.Error,都用来表示出现了异常情况,下面就针对这两种概念进行理解。
1.1 Throwable
Throwable 类是 Java 语言中所有错误(errors)和异常(exceptions)的父类。只有继承于 Throwable 的类或者其子类才能够被抛出,还有一种方式是带有 Java 中的 @throw 注解的类也可以抛出。
// 返回抛出异常的详细信息
public string getMessage();
public string getLocalizedMessage();
//返回异常发生时的简要描述
public public String toString();
// 打印异常信息到标准输出流上
public void printStackTrace();
public void printStackTrace(PrintStream s);
public void printStackTrace(PrintWriter s)
// 记录栈帧的的当前状态
public synchronized Throwable fillInStackTrace();
1.2 异常的类别
-
非正常异常(Error)
这类异常的命名以 Error 结尾,比如 OutOfMemoryError,NoSuchMethodError。这类异常,编译器编译时不检查,应用程序不需要处理,接口不需要声明,接口规范也不需要记录; -
运行时异常(RuntimeException):非检查型异常(UncheckedException)
这类异常的命名通常以 Exception 结尾,比如 IllegalArgumentException,NullPointerException。这类异常,编译器编译时不检查,接口不需要声明,但是应用程序可能需要处理,因此接口规范需要记录清楚;
// IllegalArgumentException 是运行时异常。虽然方法的声明中没有出现 IllegalArgumentException,
// 但是在方法的规范中,需要使用记录什么情况下抛出该异常。
// 只有这样,方法的调用者才能知道什么时候异常会抛出,该采取什么样的处理办法。
/**
* Check if the user name is a registered name.
*
* @return true if the userName is a registered name.
* @throws IllegalArgumentException if the user name is invalid
*/
boolean isRegisteredUser(String userName) {
// snipped
}
- 非运行时异常:检查型异常(CheckedException)
除了运行时异常之外的其他的正常异常都是非运行时异常,比如 InterruptedException,GeneralSecurityException。和运行时异常一样,命名通常以 Exception 结尾。这类异常,编译器编译时会检查异常是否已经处理或者可以抛出,接口需要声明,应用程序需要处理,接口规范需要记录清楚。
// CloneNotSupportedException 是检查型异常。这样的异常,一定要出现在对应方法的声明中。
/**
* Returns a clone if the implementation is cloneable.
*
* @return a clone if the implementation is cloneable.
*
* @throws CloneNotSupportedException if this is called on an
* implementation that does not support {@code Cloneable}.
*/
public Object clone() throws CloneNotSupportedException {
// snipped
}
1.3 异常处理原则
- 检查异常
对于检查型异常,编译器或者 IDE 会友好地提醒使用合适的声明。我们一般不会遗漏检查型异常的声明。既然声明不会遗漏,异常的标记也通常不容易遗漏。 比如上面 clone() 方法的例子,CloneNotSupportedException 已经在方法定义部分声明了。在方法规范描述部分,只要不遗漏这个异常的描述就好了。
- 运行时异常
对于运行时异常,我们就没有这么幸运了。目前我们使用的编译器或者 IDE,还没有提醒运行时异常遗漏的功能。在上面的检查用户名的例子中,如果我们不在方法的规范描述中记录抛出的运行时异常,该方法的使用立即就会遇到问题。
// 1.如果参数 userName 是一个无效引用(null),会发生什么状况,该怎么处理?
// 2.如果参数 userName 是一个空字符串(“”),会发生什么状况,该怎么处理?
// 3.如果参数 userName 不是一个规范的用户名,会发生什么状况,该怎么处理?
/**
* Check if the user name is a registered name.
*
* @return true if the userName is a registered name.
*/
boolean isRegisteredUser(String userName) {
// snipped
}
规范:
1.对于所有的可能抛出运行时异常,都要有清晰的描述,一个也不要错过;
2.查看所有的调用方法的规范描述,确认抛出的异常要么已经处理,要么已经规范描述。
1.4 处理好捕获异常
1.出了什么错: 异常类名(IllegalArgumentException, FileNotFoundException)
2.为什么会出错: 异常描述(“Invalid file path”)
3.什么地方出了错: 异常堆栈(at sun.security.ssl.InputRecord.read(InputRecord.java:504))
4.记录了不同场景对这三个问题的不同理解和不同处理:异常转换(Caused by: javax.net.ssl.SSLException: Unrecognized SSL message, plaintext connection?)
-
对于异常类名,我们要准确地选择异常类。
Exception 类是一个包罗万象的超级异常类,如果我们使用 Exception 作为声明和抛出的异常,就不方便用户精准定位,从而解读和判断“出了什么错”。 类似的超级异常类还有 RuntimeException、IOException 等。 除非是超级的接口,否则我们应该尽量减少超级异常类的使用,而是选择那些意义明确、覆盖面小的异常类,比如 FileNotFoundException. -
对于异常描述,我们要清晰地描述异常信息。
虽然 Java 异常允许使用没有具体异常信息的异常,但是这种使用却容易丢失用户对于“为什么会出错”这个问题更精准的解读。 所以我不推荐使用没有描述信息的异常。 -
对于异常转换,我们要恰当地转换异常场景
随着应用场景的转换,我们还需要转换异常的类型和描述。 比如,SQLException 这种涉及具体实现细节的异常类就不太适合直接抛给最终的用户应用。 用户关心的是商业的逻辑,并不是实现的细节,这就需要我们随着使用场景调整异常。如果一股脑儿地把所有的异常抛到底,业务逻辑就会很混乱,用户体验也不好。
1.5 不要使用异常机制处理正常业务逻辑
很多 API 的设计有检查参数有效性的方法。如果参数通过检验,就没有异常抛出,
否则就会抛出异常。在使用这个方法的代码时,我们需要检查有没有抛出异常来确认
参数是否有效。
// 异常:如果 userName 字符串不符合规范,这是一个异常状况
void checkUserName(String userName) {
// snipped
}
// 正常:如果 userName 不是一个注册用户,这通常是一个正常状况
boolean isRegisteredUser(String userName) {
// snipped
}
2、Exception关键字
throws、throw、try、finally、catch
2.1 throws 和 throw
在 Java 中,异常也就是一个对象,它能够被程序员自定义抛出或者应用程序抛出,必须借助于 throws 和 throw 语句来定义抛出异常。
throws 主要是声明这个方法会抛出这种类型的异常,使它的调用者知道要捕获这个异常。throw 是具体向外抛异常的动作,所以它是抛出一个异常实例。
throws 和 throw 通常是成对出现的,例如
static void cacheException() throws Exception{
throw new Exception();
}
2.2 try 、finally 、catch
static void cacheException() throws Exception{
for (int i = 0; i < 5; i++) {
System.out.println("enter: i=" + i);
try {
System.out.println("execute: i=" + i);
continue;
} finally {
System.out.println("leave: i=" + i);
}
}
}
2.3 关闭资源
jdk7以前:
static void tryThrowException(String path) throws Exception {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
String s = br.readLine();
System.out.println("s = " + s);
}catch (Exception e){
e.printStackTrace();
}finally {
try {
br.close();
}catch (Exception e){
e.printStackTrace();
}finally {
br.close();
}
}
}
这种写法,虽然能解决异常抛出的问题,但是各种 try-cath-finally 的嵌套会让代码变得非常臃肿。
Java7 中引入了try-with-resources 语句时:
/**
* 使用try-with-resources 改写示例一
* @param path
* @return
* @throws IOException
*/
static String firstLineOfFileAutoClose(String path) throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader(path))){
return br.readLine();
}
}
2.4 异常处理的原则
我们在日常处理异常的代码中,应该遵循三个原则
- 不要捕获类似 Exception 之类的异常,而应该捕获类似特定的异常,比如 InterruptedException,方便排查问题,而且也能够让其他人接手你的代码时,会减少骂你的次数。
- 不要生吞异常。这是异常处理中要特别注重的事情。如果我们不把异常抛出来,或者也没有输出到 Logger 日志中,程序可能会在后面以不可控的方式结束。
- 不要在函数式编程中使用 checkedException。
3、Error
大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。这些错误是不可检查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况,比如 OutOfMemoryError 和 StackOverflowError异常的出现会有几种情况
3.1 OutOfMemoryError
-
虚拟机栈:如果线程请求的栈深度大于虚拟机栈所允许的深度,将会出现 StackOverflowError 异常;如果虚拟机动态扩展无法申请到足够的内存,将出现 OutOfMemoryError。
-
本地方法栈和虚拟机栈一样
-
堆:Java 堆可以处于物理上不连续,逻辑上连续,就像我们的磁盘空间一样,如果堆中没有内存完成实例分配,并且堆无法扩展时,将会抛出 OutOfMemoryError。
-
方法区:方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。
3.2 NoClassDefFoundError 和 ClassNotFoundException 区别
在类的加载过程中, JVM 或者 ClassLoader 无法找到对应的类时,都可能会引起这两种异常/错误,由于不同的 ClassLoader 会从不同的地方加载类,有时是错误的 CLASSPATH 类路径导致的这类错误,有时是某个库的 jar 包缺失引发这类错误。NoClassDefFoundError 表示这个类在编译时期存在,但是在运行时却找不到此类,有时静态初始化块也会导致 NoClassDefFoundError 错误。
ClassLoader 是类路径装载器,在Java 中,类路径装载器一共有三种两类一种是虚拟机自带的 ClassLoader,分为三种
- 启动类加载器(Bootstrap) ,负责加载 $JAVAHOME/jre/lib/rt.jar
- 扩展类加载器(Extension),负责加载 $JAVAHOME/jre/lib/ext/*.jar
- 应用程序类加载器(AppClassLoader),加载当前应用的 classpath 的所有类
第二种是用户自定义类加载器
Java.lang.ClassLoader 的子类,用户可以定制类的加载方式。
另一方面,ClassNotFoundException 与编译时期无关,当你尝试在运行时使用反射加载类时,ClassNotFoundException 就会出现。
- NoClassDefFoundError 是 JVM 引起的错误,是 unchecked,未经检查的。因此不会使用 try-catch 或者 finally 语句块;另外,ClassNotFoundException 是受检异常,因此需要 try-catch 语句块或者 try-finally 语句块包围,否则会导致编译错误。
- 调用 Class.forName()、ClassLoader.findClass() 和 ClassLoader.loadClass() 等方法时可能会引起 java.lang.ClassNotFoundException
- NoClassDefFoundError 是链接错误,发生在链接阶段,当解析引用找不到对应的类,就会触发;而 ClassNotFoundException 是发生在运行时的异常。