定义
在程序中,如果不处理异常,正常运行的程序会完全停止运行。异常是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。
Exception:Exception又分为可检查(checked)异常和不检查(unchecked)异常。
- 可检查异常(非运行时异常),是RuntimeException以外的异常,类型上都属于Exception类和其子类,如IOException、SQLException等以及用户自定义的Exception异常。对于这种异常,java编译器强制要求我们必须对出现的这些异常进行处理,否则程序就不能编译通过。
- 不检查异常(运行时异常),其异常都是RuntimeException类及其子类,如 NullPointerException、ArrayIndexOutOfBoundsException之类,这些异常可以选择在程序里捕获处理,也可以不处理,一般是由程序逻辑错误引起的,从逻辑的角度去避免这种异常的发生,如果不处理该异常,程序会把异常一直往上层抛,一直到最上层,导致线程退出或者程序退出。
Java 异常的处理机制
Java的异常处理本质上是抛出异常和捕获异常。
- 抛出:在当前环境下无法获得必要的信息来解决问题,从当前环境中跳出,并把问题提交给上一级环境,这就是抛出异常
捕获异常
:使用try-catch块,在抛出异常的时候,将其转为寻找合适的异常处理器,来处理它,或者忽略异常,让程序继续执行。
异常处理的基础语法
Java异常处理涉及到五个关键字,分别是:try
、catch
、finally
、throw
、throws
。
try-catch
try{ //监控区域 }catch(Exception e){ //the code of handling exception1 }catch(Exception e){ //the code of handling exception2 }
try-catch
所描述的即是监控区域,关键词try
后的一对大括号将一块可能发生异常的代码包起来,即为监控区域。Java方法在运行过程中发生了异常,则创建异常对象。将异常抛出监控区域之外,由Java运行时系统负责寻找匹配的catch
子句来捕获异常。若有一个catch
语句匹配到了,则执行该catch
块中的异常处理代码,就不再尝试匹配别的catch
块了。
使用多重的catch语句
:很多情况下,由单个的代码段可能引起多个异常。处理这种情况,我们需要定义两个或者更多的catch
子句,每个子句捕获一种类型的异常,当异常被引发时,每个catch
子句被依次检查,第一个匹配异常类型的子句执行,当一个catch
子句执行以后,其他的子句将被旁路,异常类型从大到小,从上到下依次处理。
反例:Exception异常应该是放在ArrayIndexOutOfBoundsException后面,下面的写法会让Exception后面的异常处理被屏蔽掉。
反例: try{ //code that might generate exceptions }catch(Exception e){ //the code of handling exception1 }catch(ArrayIndexOutOfBoundsException e){ //the code of handling exception2 } 正确处理: try{ //code that might generate exceptions }catch(ArrayIndexOutOfBoundsException e){ //the code of handling exception1 }catch(Exception e){ //the code of handling exception2 }
嵌套try语句
:try
语句可以被嵌套。也就是说,一个try
语句可以在另一个try
块的内部。每次进入try
语句,异常的前后关系都会被推入堆栈。如果一个内部的try
语句不含特殊异常的catch
处理程序,堆栈将弹出,下一个try
语句的catch
处理程序将检查是否与之匹配。这个过程将继续直到一个catch
语句被匹配成功,或者是直到所有的嵌套try
语句被检查完毕。如果没有catch
语句匹配,Java运行时系统将处理这个异常。
try{ try{ //code that might generate exceptions }catch(IOException e){ //the code of handling exception1 } }catch(Exception e){ //the code of handling exception1 }
throw
throw 用于
抛出明确的异常。程序执行完throw
语句之后立即停止;throw
后面的任何语句不被执行,最邻近的try
块用来检查它是否含有一个与异常类型匹配的catch
语句。如果发现了匹配的块,控制转向该语句;如果没有发现,次包围的try
块来检查,以此类推。如果没有发现匹配的catch
块,默认异常处理程序中断程序的执行并且打印堆栈轨迹。
throw new NullPointerException("demo");
throws
如果一个方法会出现异常但却不处理它,那么这个方法必须明确这个异常,以使方法的调用者调用时不发生异常。要做到这点,我们可以在方法声明中包含一个throws
子句。
public void info() throws Exception
{
//body of method
throw new IllegalAccessException
(); }
info()方法里面,明确地会出现异常IllegalAccessException
,但是却不想捕获处理而是抛出去,那么它必须在方法上明确这个存在着异常。
Throws
抛出异常的规则
- 如果是不受检查异常(
unchecked exception
),即Error
、RuntimeException
或它们的子类,那么可以不使用throws
关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。 - 如果一个方法可能出现受检查异常(checked exception),要么用
try-catch
语句捕获,要么用throws
子句声明将它抛出,否则会导致编译错误 - 仅当抛出了异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣。
- 调用方法必须遵循任何可查异常的处理和声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。
finally
无论是否发生异常,我们都有需要执行的代码,这就是 final关键字的所在。例如我们在读取文件文件的时候,如果是否发生了异常,都希望最后能关闭这个文件的流。finally
子句是可选项,可以有也可以无,但是每个try
语句至少需要一个catch
或者finally
子句。
public int getPlayerScore(String playerFile) { Scanner contents; try { contents = new Scanner(new File(playerFile)); return Integer.parseInt(contents.nextLine()); } catch (FileNotFoundException noFile ) { logger.warn("File not found, resetting score."); return 0; } finally { try { if (contents != null) { contents.close(); } } catch (IOException io) { logger.error("Couldn't close the reader!", io); } } }
try-with-resources
JDK1.7之后有了try-with-resource处理机制。首先被自动关闭的资源需要实现Closeable或者AutoCloseable接口,因为只有实现了这两个接口才可以自动调用close()方法去自动关闭资源。
接口实现AutoCloseable接口,重写close方法:
public class Connection implements AutoCloseable { public void sendData() { System.out.println("正在发送数据"); } @Override public void close() throws Exception { System.out.println("正在关闭连接"); } }
调用:
public class TryWithResource { public static void main(String[] args) { try (Connection conn = new Connection()) { conn.sendData(); } catch (Exception e) { e.printStackTrace(); } } }
运行结果:
正在发送数据
正在关闭连接
原理
反编译刚才例子的class文件:
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(); } } }
编译器自动帮我们生成了finally块,并且在里面调用了资源的close方法,所以例子中的close方法会在运行的时候被执行。不过在catch(){}代码块中有一个addSuppressed()方法,即异常抑制方法。如果业务处理和关闭连接都出现了异常,业务处理的异常会抑制关闭连接的异常,只抛出处理中的异常,仍然可以通过getSuppressed()方法获得关闭连接的异常。
稍微修改一下刚才的例子:我们将刚才的代码改回远古时代手动关闭异常的方式,并且在sendData
和close
方法中抛出异常:
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"); } }
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(); } } } }
运行结果:由于我们一次只能抛出一个异常,所以在最上层看到的是最后一个抛出的异常——也就是close
方法抛出的MyException
,而sendData
抛出的Exception
被忽略了。这就是所谓的异常屏蔽。
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-with-resource的方式再次运行
public class TryWithResource { public static void main(String[] args) { try (Connection conn = new Connection()) { conn.sendData(); } catch (Exception e) { e.printStackTrace(); } } }
运行结果:异常信息中多了一个Suppressed
的提示,告诉我们这个异常其实由两个异常组成,MyException
是被Suppressed的异常。
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-with-resource的过程中,一定需要了解资源的close
方法内部的实现逻辑。否则还是可能会导致资源泄露。
举个例子,在Java BIO中采用了大量的装饰器模式。当调用装饰器的close
方法时,本质上是调用了装饰器内部包裹的流的close
方法。
public class TryWithResource { public static void main(String[] args) { try (FileInputStream fin = new FileInputStream(new File("input.txt")); GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(new File("out.txt")))) { byte[] buffer = new byte[4096]; int read; while ((read = fin.read(buffer)) != -1) { out.write(buffer, 0, read); } } catch (IOException e) { e.printStackTrace(); } } }
在上述代码中,我们从FileInputStream
中读取字节,并且写入到GZIPOutputStream
中。GZIPOutputStream
实际上是FileOutputStream
的装饰器。由于try-with-resource的特性,实际编译之后的代码会在后面带上finally代码块,并且在里面调用fin.close()方法和out.close()方法。我们再来看GZIPOutputStream
类的close方法:
public void close() throws IOException { if (!closed) { finish(); if (usesDefaultDeflater) def.end(); out.close(); closed = true; } }
我们可以看到,out变量实际上代表的是被装饰的FileOutputStream
类。在调用out变量的close
方法之前,GZIPOutputStream
还做了finish
操作,该操作还会继续往FileOutputStream
中写压缩信息,此时如果出现异常,则会out.close()
方法被略过,然而这个才是最底层的资源关闭方法。正确的做法是应该在try-with-resource中单独声明最底层的资源,保证对应的close
方法一定能够被调用。在刚才的例子中,我们需要单独声明每个FileInputStream
以及FileOutputStream
:
public class TryWithResource { public static void main(String[] args) { try (FileInputStream fin = new FileInputStream(new File("input.txt")); FileOutputStream fout = new FileOutputStream(new File("out.txt")); GZIPOutputStream out = new GZIPOutputStream(fout)) { byte[] buffer = new byte[4096]; int read; while ((read = fin.read(buffer)) != -1) { out.write(buffer, 0, read); } } catch (IOException e) { e.printStackTrace(); } } }
由于编译器会自动生成fout.close()
的代码,这样肯定能够保证真正的流被关闭。
参考: