⑤JAVA 异常
⑤JAVA 异常
学习核心
- 异常
- 介绍JAVA的异常体系?
- Exception和Error有什么区别?
- 受检和未受检异常是什么?
- 如何自定义异常?
学习资料
JAVA异常核心
1.基础概念
异常机制(异常类层次结构)
Java的异常机制能让程序具有良好的容错性,让程序更加健壮,当程序出现意外的情况,系统会自动生成一个Exception对象通知程序进程处理,从而实现业务功能和实现代码的分离。
Throwable 是 Java 语言中所有错误与异常的超类(包括Error错误和Exception异常),它包含了其线程创建时线程执行堆栈的快照,提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息
Error(错误)及其子类:程序中无法处理的错误(表示运行应用程序出现了严重的错误),此类错误一般表示代码运行时 JVM 出现问题。Error是不受检异常,非代码性错误。因此,当此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例,不应该实现任何新的Error子类的。通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如 OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。此类错误发生时,JVM 将终止线程。
Exception(异常):程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和非运行时异常(编译时异常)
- 运行时异常:RuntimeException类及其子类异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生(常见有空指针异常、下标越界异常)
- 非运行时异常(编译时异常):除RuntimeException以外的子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常
异常关键字(try、catch、finally、throw、throws)
try – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
catch – 用于捕获异常。catch用来捕获try语句块中发生的异常
finally – finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止
throw – 用于抛出异常
throws – 用在方法签名中,用于声明该方法可能抛出的异常
2.异常捕获
try...catch...捕获异常
基础语法
try {
// 业务逻辑处理
// 可能出现异常的代码;
} catch(异常类名 变量名) {
// 异常的处理代码;
}
单异常捕获(可catch多层)
public class ExceptionDemo {
public static void main(String[] args) {
/**
* 使用try...catch语句捕获异常
*/
try
{
System.out.println(9/0);
// Class.forName("java.util.ArrayListhahah");
FileInputStream fis = new FileInputStream("c:/test.txt");
}catch (ArithmeticException e) {
System.out.println("捕获到了数学运算异常的错误...");
} catch (FileNotFoundException e) {
System.out.println("捕获到了文件不存在的异常...");
} catch (Exception e) {
System.out.println("捕获到了未知的异常...");
}
}
}
多异常捕获(JAVA7提供了多异常捕获,一个catch可以捕获多个异常)
捕获多个异常,多种异常之间用|分割
捕获多种类型的异常时,异常变量有隐式的final修饰,所以程序不能对异常变量重新赋值
public class MultiExceptionDemo {
public static void main(String[] args) {
try {
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
int c = a / b;
System.out.println("当前执行的结果c是:" + c);
/**
* 一次性捕获多个异常需用 |隔开
*/
} catch (IndexOutOfBoundsException|NumberFormatException|ArithmeticException e) {
System.out.println("捕获到了数组越界,数字格式异常,算数异常 等之一的错误...");
/**
* 多异常捕获的变量是默认被final修饰的 所以下方的代码报错...
* e=new ArithmeticException("aa");
* 不能够对final修饰的变量进行二次修改赋值
*/
} catch (Exception e) { //最大的异常始终是放在最后一个捕获
System.out.println("捕获到了未知的异常...");
e=new RuntimeException("aaa");
}
}
}
访问异常信息
public class AccessExceptionDemo {
/**
* 通过相关的方法了解具体捕获到的异常信息
*/
public static void main(String[] args) {
try
{
FileInputStream fis = new FileInputStream("c://a.txt");
}catch(IOException e)
{
System.out.println(e.getClass());
System.out.println(e.getMessage());
System.out.println(e.getStackTrace());
//了解具体的异常信息
e.printStackTrace();
}
}
}
// 测试结果
class java.io.FileNotFoundException
c:/a.txt (No such file or directory)
[Ljava.lang.StackTraceElement;@5305068a
java.io.FileNotFoundException: c:/a.txt (No such file or directory)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:216)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:111)
at com.noob.base.exception.AccessExceptionDemo.main(AccessExceptionDemo.java:16)
try...catch...finally
语法
try {
//执行程序代码,可能会出现异常
} catch(Exception e) {
//捕获异常并处理
} finally {
//必执行的代码
}
执行顺序说明
- 当try没有捕获到异常时:try语句块中的语句逐一被执行,程序将跳过catch语句块,执行finally语句块和其后的语句;
- 当try捕获到异常,catch语句块里没有处理此异常的情况:当try语句块里的某条语句出现异常时,而没有处理此异常的catch语句块时,此异常将会抛给JVM处理,finally语句块里的语句还是会被执行,但finally语句块后的语句不会被执行;
- 当try捕获到异常,catch语句块里有处理此异常的情况:在try语句块中是按照顺序来执行的,当执行到某一条语句出现异常时,程序将跳到catch语句块,并与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行,而try语句块中,出现异常之后的语句也不会被执行,catch语句块执行完后,执行finally语句块里的语句,最后执行finally语句块后的语句;
try...finally
语法
try {
//执行程序代码,可能会出现异常
}finally {
//必执行的代码
}
执行顺序说明
- try块中引起异常,异常代码之后的语句不再执行,直接执行finally语句
- try块中没有引起异常,执行完try块之后就执行finally语句
应用场景
try-finally可用在不需要捕获异常的代码,可以保证资源在使用后被关闭。例如IO流中执行完相应操作后,关闭相应资源;使用Lock对象保证线程同步,通过finally可以保证锁会被释放;数据库连接代码时,关闭连接操作等等
//以Lock加锁为例,演示try-finally
ReentrantLock lock = new ReentrantLock();
try {
//需要加锁的代码
} finally {
lock.unlock(); //保证锁一定被释放
}
finally失效的场景
- 在前面的代码中用了System.exit()退出程序
- finally语句块中发生了异常
- 程序所在的线程死亡
- 关闭CPU
try-with-resource
语法(try-with-resource是Java 7中引入的)
在finally使用close方法关闭资源也可能会抛出IOException,从而覆盖了原始异常。JAVA7提供了更加优雅的方式来实现资源的自动释放,自动释放的资源需要是实现了 AutoCloseable 接口的类。
private static void tryWithResourceTest(){
try (Scanner scanner = new Scanner(new FileInputStream("c:/abc"),"UTF-8")){
// code
} catch (IOException e){
// handle exception
}
}
Scanner
public final class Scanner implements Iterator<String>, Closeable {
// ...
}
public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}
try 代码块退出时,会自动调用 scanner.close 方法,和把 scanner.close 方法放在 finally 代码块中不同的是,若 scanner.close 抛出异常,则会被抑制,抛出的仍然为原始异常。被抑制的异常会由 addSusppressed 方法添加到原来的异常,如果想要获取被抑制的异常列表,可以调用 getSuppressed 方法来获取
3.异常基础总结
关键字说明
- try、catch和finally都不能单独使用,只能是try-catch、try-finally或者try-catch-finally
- try语句块监控代码,出现异常就停止执行下面的代码,然后将异常移交给catch语句块来处理
- finally语句块中的代码一定会被执行,常用于回收资源
- throws:声明一个异常,告知方法调用者
- throw :抛出一个异常,至于该异常被捕获还是继续抛出都与它无关
JAVA常见异常类
在Java中提供了一些异常用来描述经常发生的错误,对于这些异常,有的需要进行捕获处理或声明抛出,有的是由Java虚拟机自动进行捕获处理。Java中常见的异常类:
- RuntimeException
- java.lang.ArrayIndexOutOfBoundsException 数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
- java.lang.ArithmeticException 算术条件异常。譬如:整数除零等。
- java.lang.NullPointerException 空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等
- java.lang.ClassNotFoundException 找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
- java.lang.NegativeArraySizeException 数组长度为负异常
- java.lang.ArrayStoreException 数组中包含不兼容的值抛出的异常
- java.lang.SecurityException 安全性异常
- java.lang.IllegalArgumentException 非法参数异常
- IOException
- IOException:操作输入流和输出流时可能出现的异常。
- EOFException 文件已结束异常
- FileNotFoundException 文件未找到异常
- 其他
- ClassCastException 类型转换异常类
- ArrayStoreException 数组中包含不兼容的值抛出的异常
- SQLException 操作数据库异常类
- NoSuchFieldException 字段未找到异常
- NoSuchMethodException 方法未找到抛出的异常
- NumberFormatException 字符串转换为数字抛出的异常
- StringIndexOutOfBoundsException 字符串索引超出范围抛出的异常
- IllegalAccessException 不允许访问某类异常
- InstantiationException 当应用程序试图使用Class类中的newInstance()方法创建一个类的实例,而指定的类对象无法被实例化时,抛出该异常
4.异常实践
在Java中需要思考如何处理异常、处理哪些异常、怎样处理异常,一些开发团队还会指定一些规则来规范异常处理。异常不仅仅是一个错误控制机制,也是一个通信媒介,一个团队要制定出一个最佳实践和规则、理解其通用概念,在场景中更好地使用它。
1)只针对不正常的情况才使用异常
异常只应该被用于不正常的条件,它们永远不应该被用于正常的控制流。《阿里手册》中:【强制】Java 类库中定义的可以通过预检查方式规避的RuntimeException异常不应该通过catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException等等
- 异常机制的设计初衷是用于不正常的情况,所以很少会会JVM实现试图对它们的性能进行优化。所以,创建、抛出和捕获异常的开销是很昂贵的
- 把代码放在try-catch中返回阻止了JVM实现本来可能要执行的某些特定的优化
- 对数组进行遍历的标准模式并不会导致冗余的检查,有些现代的JVM实现会将它们优化掉
2)finally块中清理资源或者使用try...with...resource语句
当使用类似InputStream这种需要使用后关闭的资源时,一个常见的错误就是在try块的最后关闭资源。
🐶错误示例参考
在try块的最后close资源,但是这段代码的逻辑是只有当try块中没有异常抛出的时候才可以正常工作,也就是说当try块中前面的代码出现异常并抛出,则这段代码可能不会执行到close部分,就会导致资源无法正常关闭。因此此处应该将资源的释放放到finally块进行处理,或者借助JAVA7的try...with...resource语句
public void doNotCloseResourceInTry() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// use the inputStream to read a file
// do NOT do this
inputStream.close();
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
更正方案1:在finally块中释放资源
public void closeResourceInFinally() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error(e);
}
}
}
}
更正方案2:JAVA7的try-with-resource 语法
如果资源实现了 AutoCloseable 接口,可以使用这个语法。大多数的 Java 标准资源都继承了这个接口。当你在 try 子句中打开资源,资源会在 try 代码块执行后或异常处理后自动关闭
public void automaticallyCloseResource() {
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
// use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
3)尽量使用标准的异常
Java标准异常中有几个是经常被使用的异常。如下表格:
异常 | 使用场合 |
---|---|
IllegalArgumentException | 参数的值不合适 |
IllegalStateException | 参数的状态不合适 |
NullPointerException | 在null被禁止的情况下参数值为null |
IndexOutOfBoundsException | 下标越界 |
ConcurrentModificationException | 在禁止并发修改的情况下,对象检测到并发修改 |
UnsupportedOperationException | 对象不支持客户请求的方法 |
重用现有的异常:
- 基于现有业务语义基础上重用异常,使得程序更具备可读性和可维护性
- 异常类越少,内存占用越小,转载类的时间开销也越小
4)对异常进行文档说明
当在方法上声明抛出异常时,也需要进行文档说明。目的是为了给调用者提供尽可能多的信息,从而可以更好地避免或处理异常。
在 Javadoc 添加 @throws 声明,并且描述抛出异常的场景
/**
* Method description
*
* @throws MyBusinessException - businuess exception description
*/
public void doSomething(String input) throws MyBusinessException {
// ...
}
例如此处案例,在抛出MyBusinessException 异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是在监控工具中,都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等
5)优先捕获最具体的异常
catch块的执行只有在匹配异常的第一个catch块才会被执行,因此在设计上可以优先捕获最具体的异常类,将不太具体的catch块添加到列表末尾
例如此处,NumberFormatException是更具体的异常提示,它是IllegalArgumentException的子类
public void catchMostSpecificExceptionFirst() {
try {
doSomething("A message");
} catch (NumberFormatException e) {
log.error(e);
} catch (IllegalArgumentException e) {
log.error(e)
}
}
6)不要捕获Throwable类
如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,指出不应该由应用程序处理的严重问题。 典型的例子是 OutOfMemoryError 或者 StackOverflowError 。两者都是由应用程序控制之外的情况引起的,无法处理。
所以,最好不要捕获 Throwable ,除非确定自己处于一种特殊的情况下能够处理错误
7)不要忽略异常
在异常处理的时候需要完善try...catch...语句,不要仅仅只是catch异常而不做任何处理。现实开发中会出现无法预料的异常,或者无法确定这里的代码未来是不是会改动(删除了阻止异常抛出的代码),而此时由于异常被捕获,使得无法拿到足够的错误信息来定位问题。合理的做法是至少要记录异常的信息,然后再思考如何处理
public void logAnException() {
try {
// do something
} catch (NumberFormatException e) {
log.error("This should never happen: " + e); // see this line
}
}
8)不要记录并抛出异常
一些代码、类库的异常处理会有捕获异常、记录日志并且直接再次抛出的逻辑。这个逻辑看似合理,但是经常会给同一个异常输出多条日志信息,但是这些日志可能没有附加更有效的信息。
try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
throw e;
}
正确的做法是仅仅当想要处理异常的时候才去捕获,否则只需要在方法签名中声明让调用者去处理。如果说非要throw异常,可以将异常类包装为自定义异常类,用于控制输出更多有效的异常信息
public void wrapException(String input) throws MyBusinessException {
try {
// do something
} catch (NumberFormatException e) {
throw new MyBusinessException("A message that describes the error.", e);
}
}
9)不要在finally块中使用return
try块中的return语句执行成功后,并不马上返回,而是继续执行finally块中的语句,如果此处存在return语句,则在此直接返回,无情丢弃掉try块中的返回点
10)不要用异常机制处理流程控制
不应该使用异常控制应用的执行流程,例如,本应该使用if语句进行条件判断的情况下,你却使用异常处理,这是非常不好的习惯,会严重影响应用的性能
5.常见异常处理误区
案例1
try {
// 业务代码
// …
Thread.sleep(1000L);
} catch (Exception e) {
// Ignore it
}
分析上述代码的不合理之处:
- 捕获异常范围太大:不建议直接捕获Exception异常,而是catch特定异常(更具体的异常)
- 生吞异常(swallow):catch异常之后却不做任何处理(例如做额外处理或者抛出自定义异常用于获取更详细的异常信息)
案例2
try {
// 业务代码
// …
} catch (IOException e) {
e.printStackTrace();
}
分析上述代码的不合理之处:
作为实验代码没有任何问题,但是在产品代码中不允许这样处理。主要问题在于printStackTrace();
,可以查看printStackTrace()文档:Prints this throwable and its backtrace to the standard error stream”。问题就在这里,在稍微复杂一点的生产系统中,标准出错(STERR)不是个合适的输出选项,因为很难判断出到底输出到哪里去了。尤其是分布式系统,如果发生异常,但是无法找到堆栈轨迹(stacktrace),这纯属是为诊断设置障碍。所以,最好使用产品日志,详细地输出到日志系统里
案例3:Throw early, catch late原则
public void readPreferences(String fileName){
//...perform operations...
InputStream in = new FileInputStream(fileName);
//...read the preferences file...
}
基于上述代码,如果fileName是null,就会抛出NullPointerException,对于这类异常其实可以通过一些预检查操作进行校验,让这个可能存在的空指针异常(NPE)更早的暴露出来,即”throw early“。在实际的业务场景中还可能是各种情况(获取配置失败等),在发现问题的时候第一时间抛出能够更加清晰地反应问题。
public void readPreferences(String filename) {
Objects. requireNonNull(filename);
//...perform other operations...
InputStream in = new FileInputStream(filename);
//...read the preferences file...
}
至于catch late,也就是捕获异常后需要怎么进行处理?最常见的误区就是生吞异常(只捕获而不处理,本质上是掩盖问题),如果实在不知道如何处理,可以保留原有异常的cause信息,直接抛出或者构建新的异常抛出去(例如抛出自定义异常)。在更高的层面上有相对清晰的业务逻辑,往往会更清楚合适的处理方式是什么。
从性能角度审视一下Java的异常处理机制,这里有两个可能会相对昂贵的地方:
- try-catch代码段会产生额外的性能开销,或者换个角度说,它往往会影响JVM对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大的try包住整段的代码;且不建议利用异常控制代码流程,它远比通常意义上的条件语句(if/else、switch)要低效。
- Java每实例化一个Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销可就不能被忽略了。
所以,对于部分追求极致性能的底层类库,有种方式是尝试创建不进行栈快照的Exception。这本身也存在争议,因为这样做的假设在于,创建异常时知道未来是否需要堆栈。问题是,实际上可能吗?小范围或许可能,但是在大规模项目中,这么做可能不是个理智的选择。如果需要堆栈,但又没有收集这些信息,在复杂情况下,尤其是类似微服务这种分布式系统,这会大大增加诊断的难度。
当服务出现反应变慢、吞吐量下降的时候,检查发生最频繁的Exception也是一种思路。
深入理解异常
1.JAVA处理异常的机制
JVM处理异常的机制:Exception Table,结合简单案例去分析
public static void simpleTryCatch() {
try {
testNPE();
} catch (Exception e) {
e.printStackTrace();
}
}
借助javap拆解class文件(需要先使用javac编译),进一步分析代码
//javap -c Main
public static void simpleTryCatch();
Code:
0: invokestatic #3 // Method testNPE:()V
3: goto 11
6: astore_0
7: aload_0
8: invokevirtual #5 // Method java/lang/Exception.printStackTrace:()V
11: return
Exception table:
from to target type
0 3 6 Class java/lang/Exception
异常表中包含了一个或多个异常处理者(Exception Handler)的信息,这些信息包含如下
- from 可能发生异常的起始点
- to 可能发生异常的结束点
- target 上述from和to之前发生异常后的异常处理者的位置
- type 异常处理者处理的异常的类信息
异常表在什么时候会被用到?=》当一个异常发生的时候
- JVM会在当前出现异常的方法中,查找异常表,是否有合适的处理者来处理
- 如果当前方法异常表不为空,并且异常符合处理者的from和to节点,并且type也匹配,则JVM调用位于target的调用者来处理。
- 如果上一条未找到合理的处理者,则继续查找异常表中的剩余条目
- 如果当前方法的异常表无法处理,则向上查找(弹栈处理)刚刚调用该方法的调用处,并重复上面的操作
- 如果所有的栈帧被弹出,仍然没有处理,则抛给当前的Thread,Thread则会终止
- 如果当前Thread为最后一个非守护线程,且未处理异常,则会导致JVM终止运行
2.异常耗时分析
说用异常慢,首先来看看异常慢在哪里?有多慢?构建测试用例简单的测试了建立对象、建立异常对象、抛出并接住异常对象三者的耗时对比
public class ExceptionTimeConsumeDemo {
private int testTimes;
public ExceptionTimeConsumeDemo(int testTimes) {
this.testTimes = testTimes;
}
public void newObject() {
long l = System.nanoTime();
for (int i = 0; i < testTimes; i++) {
new Object();
}
System.out.println("建立对象:" + (System.nanoTime() - l));
}
public void newException() {
long l = System.nanoTime();
for (int i = 0; i < testTimes; i++) {
new Exception();
}
System.out.println("建立异常对象:" + (System.nanoTime() - l));
}
public void catchException() {
long l = System.nanoTime();
for (int i = 0; i < testTimes; i++) {
try {
throw new Exception();
} catch (Exception e) {
}
}
System.out.println("建立、抛出并接住异常对象:" + (System.nanoTime() - l));
}
public static void main(String[] args) {
ExceptionTimeConsumeDemo test = new ExceptionTimeConsumeDemo(10000);
test.newObject();
test.newException();
test.catchException();
}
}
// 测试响应结果
建立对象:247666
建立异常对象:4084541
建立、抛出并接住异常对象:5064500
建立一个异常对象,是建立一个普通Object耗时的约20倍(实际上差距会比这个数字更大一些,因为循环也占用了时间,追求精确可以再测一下空循环的耗时然后在对比前减掉这部分),而抛出、接住一个异常对象,所花费时间大约是建立异常对象的4倍