相信你们天天都在使用Java异常机制,也相信你们对try-catch-finally执行流程烂熟于胸。本文将介绍Java异常机制的一些细节问题,这些问题虽然很小,但对代码性能、可读性有着较为重要的做用。java
在学习一项技术前,必定要先站在制高点俯瞰技术全局,从宏观上把控某项技术的整个脉络结构。这样你就能够有针对性地学习该体系结构中最重要的知识点,而且在学习细节的时候不至于钻入牛角尖。因此,在介绍Java异常你所不知道的一些秘密以前,先让你们复习一下Java异常体系。api
Throwable是整个Java异常体系的顶层父类,它有两个子类,分别是:Error和Exception。app
Error表示系统致命错误,程序没法处理的错误,好比OutOfMemoryError、ThreadDeath等。这些错误发生时,Java虚拟机只能终止线程。ide
Exception是程序自己能够处理的异常,这种异常分两大类运行时异常和非运行时异常。函数
运行时异常都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等,这些异常属于unchecked异常,开发人员能够选择捕获处理,也能够不处理。这些异常通常是由程序逻辑错误引发的,程序应该从逻辑角度尽量避免这类异常的发生。性能
在Exception异常体系中,除了RuntimeException类及其子类的异常,均属于checked异常。当你调用了抛出这些异常的方法后,必需要处理这些异常。若是不处理,程序就不能编译经过。如:IOException、SQLException、用户自定义的Exception异常等。学习
在JDK 1.7以前,处理IO操做很是麻烦。因为IOException属于checked异常,调用者必须经过try-catch处理他们;又由于IO操做完成后须要关闭资源,然而关闭资源的close()方法也会抛出checked异常,所以也须要使用try-catch处理该异常。所以,本来小小的一段IO操做代码会被复杂的try-catch嵌套包裹,从而极大地下降了程序的可读性。spa
一个标准的IO操做代码以下:线程
public class Demo {
public static void main(String[] args) {
BufferedInputStream bin = null;
BufferedOutputStream bout = null;
try {
bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")));
int b;
while ((b = bin.read()) != -1) {
bout.write(b);
}
}
catch (IOException e) {
e.printStackTrace();
}
finally {
if (bin != null) {
try {
bin.close();
}
catch (IOException e) {
e.printStackTrace();
}
finally {
if (bout != null) {
try {
bout.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
}
复制代码
上述代码使用一个输出流bin和一个输入六bout,将一个文件中的数据写入另外一个文件。因为IO资源很是宝贵,所以在完成操做后,必须在finally中分别释放这两个资源。而且为了可以正确释放这两个IO资源,须要用两个finally代码块嵌套的方式完成资源的释放。设计
在上述40行代码中,真正处理IO操做的代码不到10行,而其他30行代码都是用于保证资源合理释放的。这显然致使代码可读性较差。不过好在JDK 1.7提供了try-with-resources解决这一问题。修改后的代码以下:
public class TryWithResource {
public static void main(String[] args) {
try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
int b;
while ((b = bin.read()) != -1) {
bout.write(b);
}
}
catch (IOException e) {
e.printStackTrace();
}
}
}
复制代码
咱们须要将资源声明代码放入try后的括号中,而后将资源处理代码放入try后的{}中,catch代码块中仍然进行异常处理,而且无需写finally代码块。
那么,try-with-resources为何可以避免大量资源释放代码呢?答案是,由Java编译器来帮咱们添加finally代码块。注意,编译器只会添加finally代码块,而资源释放的过程须要资源提供者提供。
在JDK 1.7中,全部的IO类都实现了AutoCloseable
接口,而且须要实现其中的close()
函数,资源释放过程须要在该函数中完成。
那么,编译器在编译时,会自动添加finally代码块,并将close()
函数中的资源释放代码加入finally代码块中。从而提升代码可读性。
在try-catch-finally代码块中,若是try块、catch块和finally块均有异常抛出,那么最终只能抛出finally块中的异常,而try块和catch块中的异常将会被屏蔽。这就是异常屏蔽问题。以下面代码所示:
public class Connection implements AutoCloseable {
public void sendData() throws Exception {
throw new Exception("send data");
}
@Override
public void close() throws Exception {
throw new MyException("close");
}
}
复制代码
首先定义一个Connection
类,该类提供了sendData()
和close()
方法,为了实验须要,这两个方法没有任何业务逻辑,都直接抛出一个异常。下面咱们使用这个类。
public class TryWithResource {
public static void main(String[] args) {
try {
test();
}
catch (Exception e) {
e.printStackTrace();
}
}
private static void test() throws Exception {
Connection conn = null;
try {
conn = new Connection();
conn.sendData();
}
finally {
if (conn != null) {
conn.close();
}
}
}
}
复制代码
当执行conn.sendData()
时,该方法会将异常抛给调用者main()
,但在拋以前会先执行finally块。当执行finally块中的conn.close()
方法时,也会向调用者抛一个异常。此时,由try块抛出的异常将会被覆盖,main方法中仅打印finally块中的异常。其结果以下所示:
basic.exception.MyException: close
at basic.exception.Connection.close(Connection.java:10)
at basic.exception.TryWithResource.test(TryWithResource.java:82)
at basic.exception.TryWithResource.main(TryWithResource.java:7)
......
复制代码
这就是try-catch-finally的异常屏蔽问题,而try-with-resources能很好地解决这一问题。那么,它是如何解决这一问题的呢?
咱们首先将这段代码用try-with-resources改写:
public class TryWithResource {
public static void main(String[] args) {
try {
test();
}
catch (Exception e) {
e.printStackTrace();
}
}
private static void test() throws Exception {
Connection conn = null;
try (conn = new Connection();) {
conn.sendData();
}
}
}
复制代码
为了能清楚地了解Java编译器在try-with-resources上所作的事情,咱们反编译这段代码,获得以下代码:
public class TryWithResource {
public TryWithResource() {
}
public static void main(String[] args) {
try {
// 资源声明代码
Connection e = new Connection();
Throwable var2 = null;
try {
// 资源使用代码
e.sendData();
} catch (Throwable var12) {
var2 = var12;
throw var12;
} finally {
// 资源释放代码
if(e != null) {
if(var2 != null) {
try {
e.close();
} catch (Throwable var11) {
var2.addSuppressed(var11);
}
} else {
e.close();
}
}
}
} catch (Exception var14) {
var14.printStackTrace();
}
}
}
复制代码
最核心的操做是22行var2.addSuppressed(var11);
。编译器将try块和catch块中的异常先存入一个局部变量,当finally块中再次抛出异常时,经过以前异常的addSuppressed()
方法将当前异常添加至其异常栈中,从而保证了try块和catch块中的异常不丢失。当使用了try-with-resources后,输出结果以下所示:
java.lang.Exception: send data
at basic.exception.Connection.sendData(Connection.java:5)
at basic.exception.TryWithResource.main(TryWithResource.java:14)
......
Suppressed: basic.exception.MyException: close
at basic.exception.Connection.close(Connection.java:10)
at basic.exception.TryWithResource.main(TryWithResource.java:15)
... 5 more
复制代码
众所周知,首先执行try中代码,若未发生异常,则直接执行finally中代码;若发生异常,则先执行catch中代码后,再执行finally中代码。
相信上述流程你们都烂熟于胸,但若是try块和catch块中出现了return呢?出现了throw呢?此时执行顺序就会发生变化。
可是,万变不离其中,你们只要记住一点:fianlly中的return、throw会覆盖try、catch中的return、throw。此话怎讲?请继续往下阅读。
要解释这个问题,先来看一个例子,请问下面代码中的test()函数会返回什么结果?
public int test() {
try {
int a = 1;
a = a / 0;
return a;
} catch (Exception e) {
return -1;
} finally{
return -2;
}
}
复制代码
答案是-2。
当执行代码a = a / 0;
时发生异常,try块中它以后的代码便再也不执行,而是直接执行catch中代码; 在catch块中,当在执行return -1
前,先会执行finally块; 因为finally块中有return语句,所以catch中的return将会被覆盖,直接执行fianlly中的return -2
后程序结束。所以输出结果是-2。
一样地,将return换成throw也是同样的结果,finally会覆盖try、catch块中的return、throw。
特别提醒:禁止在finally块中使用return语句!这里举例子只是告诉你Java的这一特性,在实际开发中禁止使用!
空指针异常是一个运行时异常,对于这一类异常,若是没有明确的处理策略,那么最佳实践在于让程序早点挂掉,可是不少场景下,不是开发人员没有具体的处理策略,而是根本没有意识到空指针异常的存在。当异常真的发生的时候,处理策略也很简单,在存在异常的地方添加一个if语句断定便可,可是这样的应对策略会让咱们的程序出现愈来愈多的null断定,咱们知道一个良好的程序设计,应该让代码中尽可能少出现null关键字,而java8所提供的Optional类则在减小NullPointException的同时,也提高了代码的美观度。但首先咱们须要明确的是,它并 不是对null关键字的一种替代,而是对于null断定提供了一种更加优雅的实现,从而避免NullPointException。
假设存在以下Person类:
class Person{
private long id;
private String name;
private int age;
// 省略setter、getter
}
复制代码
当咱们调用某一个接口,获取到一个Person对象,此时能够经过以下方法对它进行处理:
ofNullable(person)
Optional<Person> personOpt = Optional.ofNullable(person);
复制代码
T orElse(T other)
personOpt.orElse(new Person("柴毛毛"));
复制代码
T orElseGet(Supplier<? extends T> other)
personOpt.orElseGet(()->{
Person person = new Person();
person.setName("柴毛毛");
person.setAge(20);
return person;
});
复制代码
<X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier)
personOpt.orElseThrow(CommonBizException::new);
复制代码
<U>Optional<U> map(Function<? super T,? extends U> mapper)
String name = personOpt
.map(Person::getName)
.orElseThrow(CommonBizException::new)
.map(Optional::get);
复制代码
if (obj != null) {...}
try { obj.method() } catch (NullPointerException e) {...}