异常概念
异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。
比如说,你的代码少了一个分号,那么运行出来结果是提示是错误java.lang.Error;如果你用System.out.println(11/0),那么你是因为你用0做了除数,会抛出java.lang.ArithmeticException的异常。
异常发生的原因有很多,通常包含以下几大类:
用户输入了非法数据。
要打开的文件不存在。
网络通信时连接中断,或者JVM内存溢出。
这些异常有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的。-
要理解Java异常处理是如何工作的,你需要掌握以下三种类型的异常:
检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。
异常类的继承体系
从上面可知,所有的异常类都是Throwable的子类,Throwable派生了Error和Exception这两个子类
Error(错误):所有的子类及本身的实例,代表了虚拟机相关的错误,比如系统崩溃、虚拟机错误等等。此类错误是不能被开发者通过代码捕获处理的,正常情况下Error是很少出现的。
Exception(异常):所有的子类及本身,代表程序运行时发生了各种不期待的事件,这些事件能被java异常处理机制捕获处理,是异常处理的核心。
异常处理的基本语法
在捕获处理异常的编码时,有两种处理方式:
- 使用try…catch…finally语句处理
- 在函数名中使用throws声明交给函数调用者解决
try…catch…finally语句块
try{ //try块中放可能发生异常的代码。 //如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。 //如果发生异常,则尝试去匹配catch块。 }catch(SQLException SQLexception){ //每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中可以将多个异常声明在一个catch中。 //catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。 //在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。 //如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。 //如果try中没有发生异常,则所有的catch块将被忽略。 }catch(Exception exception){ //... }finally{ //finally块通常是可选的。 //无论异常是否发生,异常是否匹配被处理,finally都会执行。 //一个try至少要有一个catch块,否则, 至少要有1个finally块。但是finally不是用来处理异常的,finally不会捕获异常。 //finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。 }
在异常捕获语法结构上,try块是必需的,而catch和finally必须选择其中某一个进行组合,否则编译会报错的。
try { } catch (Exception e) { } try { }finally{ }
例子:
public class ExceptionTest { public String test; public static void main(String[] args) { try { //空指针异常 ExceptionTest exceptionTest=null; System.out.println(exceptionTest.test); //可能数组越界异常…… 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("算术异常:比如6/0"); }catch (NullPointerException nue) { System.out.println("空指针异常:对象为null"); }catch (Exception e) { System.out.println("未知异常"); } } }
上面的代码存在两处异常,在执行运行时只会抛出第一个异常点,也就是说当try中的代码只要有一处出现了异常,该处后面的代码是不会在执行了,同时需要注意的是,异常捕获时,不仅要把Exception类对应的catch块放在最后,而且还需把Exception的子类排放在Exception类的前面,否则会报编译错误,总之在异常捕获时,要先捕获小异常,再捕获大异常。在java 7中开始引入了多异常捕获的功能,如下:
try { //空指针异常 ExceptionTest exceptionTest=null; System.out.println(exceptionTest.test); //可能数组越界异常…… int a=Integer.parseInt(args[0]); int b=Integer.parseInt(args[1]); int c=a/b; System.out.println("两个数相除的结果:"+c); //此处是多异常捕获,类型之间用"|"隔开 } catch(IndexOutOfBoundsException | NullPointerException ine){ System.out.println("数组越界或者空指针异常"); }
finally块
不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally总会被执行的,一般情况finally块不会被执行,就是System.exit()被调用了或者try块没有被执行。try块一般是业务代码的实现以一些资源的打开(网络的链接,磁盘文件的读取),通常资源的关闭会在finally块中执行。
public static void testFinally() { FileInputStream fileInputStream = null; try { fileInputStream = new FileInputStream("a.txt"); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { //资源的回收工作 try { if (fileInputStream != null) { //关闭资源 fileInputStream.close(); } } catch (IOException e) { e.printStackTrace(); } System.out.println("finally块里的资源回收"); } } }
使用throws声明异常
使用throws声明抛出异常的 基本思路是,当前方法不处理或者不知道如何处理这种类型的异常,会把该类型异常抛给调用者处理,如果调用者还是无法处理,会继续把异常抛给main方法,若main方法也不知道如何处理,最终会交给虚拟机JVM处理,而JVM处理的方式,就是中止程序运行。
public static void main(String[] args) throws FileNotFoundException { testThrows(); } public static void testThrows() throws FileNotFoundException{ //因为FileInputStream的构造函数声明了抛出FileNotFoundException //所以FileInputStream的代码需要放在try……catch中,要么使用throws声明抛出异常 //这里使用throws声明 FileInputStream fileInputStream = new FileInputStream("a.txt"); }
使用throw抛出异常语句
使用者可以在可能出现的异常处,通过throw语句手动抛出一个异常,throw语句后面不是一个异常类,是一个异常对象。
private static void testThrow(User user) { if (user==null) { throw new NullPointerException("用户不存在"); } }
Exception in thread "main" java.lang.NullPointerException: 用户不存在 at exception.ExceptionTest.testThrow(ExceptionTest.java:48) at exception.ExceptionTest.main(ExceptionTest.java:42)
自定义异常类
自定义异常类都会继承于Exception基类进行扩展,如果希望自定义运行时的异常,则需要继承于RuntimeException为基类。一般自定义异常类会提供两个构造函数,一个无参的,另外一个带字符串形参,字符串主要是描述具体异常信息,可以try…catch的方式捕获异常在catch块中通过getMessage()得到异常信息具体内容。
//自定义异常类,继承于运行时RuntimeException类 public static class ActionException extends RuntimeException{ private static final long serialVersionUID = 1L; //无参构造函数 public ActionException() { super(); } //带字符串形参的构造函数,message主要是对异常的描述 public ActionException(String message) { super(message); } } public static void testCustomException(User user){ if (user==null) { throw new ActionException("用户信息不能为null"); } } public class User{ }
try { testCustomException(null); } catch (Exception e) { String message = e.getMessage(); System.out.println(message); }
finally块与return
1.finally块何时执行
前面提到了只要有两种情况finally块是不执行的,一种是JVM退出了,另外一种是try块没有被执行到,就是提前结束了。也就是说在没有出现上面两种情况的前提下,无论如何不管是否存在异常的抛出,finally代码块都会被执行。
public static void test1(){ try { System.out.println("test1:我在try块中"); System.exit(0); } catch (Exception e) { System.out.println("test1:我在catch块中"); //在没有异常抛出的前提下,System.exit()方法放在catch块是没有效果的 // System.exit(0); }finally{ System.out.println("test1:我在finally块中"); } // 输出:test1:我在try块中 } public static void test2(){ test2_2(); try { System.out.println("test2:我在try块中"); System.exit(0); } catch (Exception e) { System.out.println("test2:我在catch块中"); }finally{ System.out.println("test2:我在finally块中"); } //没有输出结果 } public static void test2_2(){ return; }
2. finally块与return的执行顺序
finally与returm的使用,会使整个代码逻辑变的相对复杂些,如果稍微不注意可能会达不到自己想要的效果,下面是自己测试总结的几个点,可能不是很全面。
2.1. 在没有返回结果的方法,try块中使用return时,finally块中修改会影响改变值(也是最终值),同时try…catch…finally块后面的代码是不会被执行。
public static void test3(){ int a=0; try { a=10; System.out.println("try: "+a); return; } catch (Exception e) { e.printStackTrace(); }finally{ a=20; System.out.println("finally: "+a); } System.out.println("end: "+a); /** * 输出结果: * try: 10 finally: 20 */ }2.2. 在有返回结果的方法中,在finally块中会改变其值,但未必就是最终的返回值,主要要看return方法的调用位置是如何。
测试1: 在finally代码块中没有return返回值
public static void main(String[] args) { System.out.println("result: "+test4()); } //使用@SuppressWarnings阻止警告提示 @SuppressWarnings("finally") public static int test4(){ int a=0; try { a=10; System.out.println("try: "+a); return a; } catch (Exception e) { e.printStackTrace(); }finally{ a=20; System.out.println("finally: "+a); } System.out.println("end: "+a); return a; }
输出结果:
try: 10 finally: 20 result: 10测试2:在finally代码块中有return返回值
public static void main(String[] args) { System.out.println("result: "+test5()); } //使用@SuppressWarnings阻止警告提示 @SuppressWarnings("finally") public static int test5(){ int a=0; try { a=10; System.out.println("try: "+a); return a; } catch (Exception e) { e.printStackTrace(); }finally{ a=20; System.out.println("finally: "+a); return a; } // System.out.println("end: "+a); // return a; }
输出结果:
try: 10 finally: 20 result: 20
从上面的结果可以看出,也验证了2.2的结论,在测试2用例,需要注意一点就是,如果在finally使用了return,异常代码块后面试不允许存在代码的,否则会编译报错。
2.3.当发生异常后,catch块中的return和未发生异常的try块中的return返回情况是完全一样的
public static void test6() { int a = 0; try { a=10/0; System.out.println("try: " + a); } catch (Exception e) { a = 10; System.out.println("catch: " + a); return; } finally { a = 20; System.out.println("finally: " + a); } System.out.println("end: " + a); /** * 输出结果: catch: 10 finally: 20 */ }
关于finally与return执行顺序,不单单只有上面这几种情况。基于于此个人总结如下
- 尽量不要把return放在try和finally块中,最好把return放在try…catch…finally后面
- 在finally块尽量不要做太多的工作,最好是用来释放资源。
参考了如下