Java异常处理
Java的基本理念是:结构不佳的代码不能运行。
程序员做开发时,往往只做了对的事情。
然而,只做对的的事情是远远不够的,但是,我们也无法穷举所有的异常情况,所以,我们需要异常处理机制。
异常处理流程:
异常捕获的例子:
try { int a = Integer.parseInt(args[0]); int b = Integer.parseInt(args[1]); int c = a / b; System.out.println("您输入的两个数相除的结果是:" + c ); } catch (IndexOutOfBoundsException ie) { System.out.println("数组越界:运行程序时输入的参数个数不够"); } catch (NumberFormatException ne) { System.out.println("数字格式异常:程序只能接受整数参数"); } catch (ArithmeticException ae) { System.out.println("算术异常"); } catch (Exception e) { System.out.println("未知异常"); }
java异常类的继承关系:
处理多个异常时,应当Exception类的catch块放到最后,所有父类异常的catch块都应放在子类异常的catch块之后,遵循先处理小异常,在处理大异常。
从Java7开始,一个catch块可以捕获多种类型的异常。
捕获多种类型异常时要注意:
多种异常类型之间用 | 隔开
异常变量有隐式final修饰,因此程序不能对异常变量重新赋值。而捕获一种类型的异常时,异常变量没有final修饰。
try { int a = Integer.parseInt(args[0]); int b = Integer.parseInt(args[1]); int c = a / b; System.out.println("您输入的两个数相除的结果是:" + c ); } catch (IndexOutOfBoundsException|NumberFormatException |ArithmeticException ie) { System.out.println("程序发生了数组越界、数字格式异常、算术异常之一"); // 捕捉多异常时,异常变量默认有final修饰, // 所以下面代码有错: ie = new ArithmeticException("test"); // ① } catch (Exception e) { System.out.println("未知异常"); // 捕捉一个类型的异常时,异常变量没有final修饰 // 所以下面代码完全正确。 e = new RuntimeException("test"); // ② }
编译工具也会有提示。
访问异常信息:
Java程序可以通过访问catch块后的异常形参来获得异常对象的相关信息,当Java运行时决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,程序即可通过该参数来获得异常的相关信息。
所有异常对象都包含了如下几种常用方法:
>getMessage():返回该异常的详细描述字符串。
>printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。
>printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。
>getStackTrace:返回该异常的跟踪栈信息。
try { FileInputStream fis = new FileInputStream("a.txt"); } catch (IOException ioe) { System.out.println("getMessage()"+ioe.getMessage());//返回该异常的详细描述字符串。 System.out.println("getStackTrace()"+ioe.getStackTrace());//返回该异常的跟踪栈信息。 ioe.printStackTrace();//将该异常的跟踪栈信息输出到标准错误输出。 ioe.printStackTrace(new PrintStream("text.txt"));//将该异常的跟踪栈信息输出到指定输出流。 }
使用finally回收资源
在异常处理中,不管try块中的代码是否出现了异常,也不管哪一个catch块被执行,甚至在try块或者catch块中执行了return语句,finally总会被执行,所以可以再finally块中回收系统资源。
例子:
FileInputStream fis = null; try { fis = new FileInputStream("a.txt"); } catch (IOException ioe) { System.out.println(ioe.getMessage()); // return语句强制方法返回 return ; // ① // 使用exit来退出虚拟机 // System.exit(1); // ② } finally { // 关闭磁盘文件,回收资源 if (fis != null) { try { fis.close(); } catch (IOException ioe) { ioe.printStackTrace(); } } System.out.println("执行finally块里的资源回收!"); }
以上代码在catch块中有return语句,强制返回,但是下面的finally中的语句还是会执行的。
但是,如果使用System.exit(1);语句来退出虚拟机,finally中的代码将不会执行。
FileInputStream fis = null; try { fis = new FileInputStream("a.txt"); } catch (IOException ioe) { System.out.println(ioe.getMessage()); // return语句强制方法返回 //return ; // ① // 使用exit来退出虚拟机 System.exit(1); // ② } finally { // 关闭磁盘文件,回收资源 if (fis != null) { try { fis.close(); } catch (IOException ioe) { ioe.printStackTrace(); } } System.out.println("执行finally块里的资源回收!"); }
一般不要在finally中使用return或者throw等导致方法终止的语句,否则会导致try块、catch块中的return、throw语句失效。
public static void main(String[] args) throws Exception { boolean a = test(); System.out.println(a); } public static boolean test() { try { // 因为finally块中包含了return语句 // 所以下面的return语句失去作用 return true; } finally { return false; } }
以上会执行finally中的return false,所以在try中的true会失效。
通常没有必要在使用超过两层的嵌套异常处理。
Java9增强的自动关闭资源的try语句。
Java7允许在try关键字后面紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,这里的资源指的是那些必须在程序结束时显示关闭的资源(数据库连接,网络连接等),try语句在该语句结束时自动关闭这些资源。
为了保证try语句可以正常关闭资源,这些资源实现类必须实现AutoCloseable或Closeable借口,实现这两个接口就必须事先close()方法。
使用自动关闭资源的try语句:
public static void main(String[] args) throws IOException { try ( // 声明、初始化两个可关闭的资源 // try语句会自动关闭这两个资源。 BufferedReader br = new BufferedReader( new FileReader("AutoCloseTest.java")); PrintStream ps = new PrintStream(new FileOutputStream("a.txt"))) { // 使用两个资源 System.out.println(br.readLine()); ps.println("庄生晓梦迷蝴蝶"); } }
Java9再次增强了这种try语句,Java9不要求在try语句后面圆括号内声明并创建资源,只需要自动关闭资源有finally修饰或者有效的final,Java9允许将资源变量放在try后的圆括号内。上面的程序在Java9中可以改为:
public static void main(String[] args) throws IOException { // 有final修饰的资源 final BufferedReader br = new BufferedReader( new FileReader("AutoCloseTest.java")); // 没有显式使用final修饰,但只要不对该变量重新赋值,按该变量就是有效的final PrintStream ps = new PrintStream(new FileOutputStream("a.txt")); // 只要将两个资源放在try后的圆括号内即可 try (br;ps) { // 使用两个资源 System.out.println(br.readLine()); ps.println("庄生晓梦迷蝴蝶"); } }
使用throws声明抛出异常
一般如果当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理。
注意:如果main方法也不知道如何处理异常也可以用throws声明抛出异常,该异常将讲给JVM处理。JVM会打印异常的跟踪栈信息,并终止程序运行,这就是程序遇到异常后自动结束的原因。
一旦用throws抛出异常,就不用try{}catch{}处理异常。
方法重写时声明抛出异常要注意:
子类方法声明抛出的异常类型应该是父类方法声明抛出异常类型的子类或者相同。子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。
使用throw抛出异常:
程序中可以使用throw自行抛出异常,当Java运行时接收到开发者自行抛出的异常时,同样会终止当前的执行流,跳到该异常对应的catch块,由该catch块来处理该异常。不管系统自动抛出的异常,还是程序员手动抛出的异常,Java运行时环境对异常的处理没有差别。
自定义异常
用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。定义异常类时通常需要提供两个构造器:一个无参构造器,一个带一个字符串参数的构造器,这个字符串将作为该异常的描述信息(也就是getMessage()的返回值)。
自定义异常类:
// 无参数的构造器 public AuctionException(){} //① // 带一个字符串参数的构造器 public AuctionException(String msg) //② { super(msg); }
catch和throw同时使用:
当一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可完全处理该异常。在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以再次抛出异常,让该方法的调用者也能捕获到异常。
private double initPrice = 30.0; // 因为该方法中显式抛出了AuctionException异常, // 所以此处需要声明抛出AuctionException异常 public void bid(String bidPrice) throws AuctionException { double d = 0.0; try { d = Double.parseDouble(bidPrice); } catch (Exception e) { // 此处完成本方法中可以对异常执行的修复处理, // 此处仅仅是在控制台打印异常跟踪栈信息。 e.printStackTrace(); // 再次抛出自定义异常 throw new AuctionException("竞拍价必须是数值," + "不能包含其他字符!"); } if (initPrice > d) { throw new AuctionException("竞拍价比起拍价低," + "不允许竞拍!"); } initPrice = d; } public static void main(String[] args) { AuctionTest at = new AuctionTest(); try { at.bid("df"); } catch (AuctionException ae) { // 再次捕捉到bid方法中的异常。并对该异常进行处理 System.err.println(ae.getMessage()); } }
Java7增强的throw语句
在Java7以前:
public static void main(String[] args) // Java 6认为①号代码可能抛出Exception, // 所以此处声明抛出Exception throws Exception { try { new FileOutputStream("a.txt"); } catch (Exception ex) { ex.printStackTrace(); throw ex; // ① } }
在Java7以后:
public static void main(String[] args) // Java 6认为①号代码可能抛出Exception, // 所以此处声明抛出Exception // throws Exception // Java 7会检查①号代码可能抛出异常的实际类型, // 因此此处只需声明抛出FileNotFoundException即可。 throws FileNotFoundException { try { new FileOutputStream("a.txt"); } catch (Exception ex) { ex.printStackTrace(); throw ex; // ① } }
从Java7开始,Java编译器会执行更细致的检查,Java编译器会检查throw语句抛出异常的实际类型。所以,即使catch语句中用的是Exception异常,main方法抛出的是实际的异常类型FileNotFoundException。
异常链
如图所示的应用,当业务逻辑层访问数据库层出现SQLException异常时,程序不应该把底层的SQLException异常传到用户界面,因为一方面用户看到SQLException异常对用户使用系统没有帮助,用户也看不懂,另一方面,不安全。
通常的做法是,程序捕获原始异常,然后抛出一个新的业务异常,新的异常包含了给用户的提示信息,这种处理方法被称为异常转译。
这种吧原始异常信息隐藏,仅向上提供必要异常提示信息的处理方式,可以保证底层异常不会扩散到表现层,可以避免向上暴露太多细节,这符合面向对象的封装原则。
这种把捕获一个异常然后接着抛出另一个异常,并把原始异常信息保存下来是一种典型的链式处理,也被称为异常链,也是23种设计模式之一:责任链模式。
捕获异常后通过抛出SalException异常来抛出提示信息或记录信息
public class SalException extends Exception { public SalException(){} public SalException(String msg) { super(msg); } // 创建一个可以接受Throwable参数的构造器 public SalException(Throwable t) { super(t); } }
Java的异常跟踪栈
异常对象的 .printStackTrace() 方法用于打印异常的跟踪栈信息,根据 .printStackTrace() 方法的输出结果,可以找到异常的源头,并跟踪到异常一路触发的过程。、
class SelfException extends RuntimeException { SelfException(){} SelfException(String msg) { super(msg); } } public class Test { public static void main(String[] args) { firstMethod(); } public static void firstMethod() { secondMethod(); } public static void secondMethod() { thirdMethod(); } public static void thirdMethod() { throw new SelfException("自定义异常信息"); } }
在面向对象编程中,为了实现更好的重用性,大多数复杂的方法会被分解成一系列方法调用。面向对象程序运行时,经常会发生一系列方法调用,从而形成方法调用栈,异常的传播则相反:只要异常没有被完全捕获,异常就会向外传播,直到最后也没有处理该异常,直到JVM终止该程序,并打印异常的跟踪栈信息。
多线程程序中发生异常:
public class Test implements Runnable{ public void run() { firstMethod(); } public void firstMethod() { secondMethod(); } public void secondMethod() { int a = 5; int b = 0; int c = a / b; } public static void main(String[] args) { new Thread(new Test()).start(); } }
程序在Thread的run方法中出现了异常,这个异常源自 secondMethod() 方法,这个异常传播到run方法就会结束。