Java基础系列5:深入理解Java异常体系
Posted 说故事的五公子
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java基础系列5:深入理解Java异常体系相关的知识,希望对你有一定的参考价值。
该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。
前言:
Java的基本理念是“结构不佳的代码不能运行”。
“异常”这个词有“我对此感到意外”的意思。问题出现了,你也许不清楚该如何处理,但你的确知道不应该置之不理;你要停下来,看看是不是有别人或在别的地方,能够处理这个问题。只是在当前的环境中还没有足够的信息来解决这个问题,所以就把这个问题提交到一个更高级别的环境中,在这里将作出正确的决定。
使用异常所带来的另一个相当明显的好处是,它往往能够降低错误处理代码的复杂度。如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它。而如果使用异常,那就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误。并且,只需在一个地方处理错误,即所谓的异常处理程序中。这种方式不仅节省代码,而且把“描述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离。总之,与以前的错误处理方法相比,异常机制使代码的阅读、编写和调试工作更加井井有条。
异常概述:
现在我们需要编写一个五子棋程序,当用户输入下期坐标时,程序要判断用户输入是否合法,如果保证程序有较好的容错性,将会有如下的代码(伪代码):
if(用户输入包含除逗号之外的其他非数字字符) { alert 坐标只能是数值 goto retry } else if(用户输入不包含逗号) { alert 应使用逗号分隔两个坐标值 goto retry } else if(用户输入坐标值超出了有效范围) { alert 用户输入坐标应位于棋盘坐标之内 goto retry } else if(用户输入的坐标已有棋子) { alert 只能在没有棋子的地方下棋 goto retry } else { ..... }
上面代码还未涉及任何有效处理,只是考虑了4种可能的错误,代码就已经急剧增加了。但实际上,上面考虑的4种情形还远未考虑到所有的可能情形(事实上,世界上的意外是不可穷举的),程序可能发生的异常情况总是大于程序员所能考虑的意外情况。
对于上面的错误处理机制,主要有以下两个缺点:
- 无法穷举所有的异常情况。因为人类知识的限制,异常情况总比可以考虑到的情况多,总有“漏网之鱼”的异常情况,所以程序总是不够健壮。
- 错误处理代码和业务实现代码混杂。这种错误处理和业务实现混杂的代码严重影响程序的可读性,会增加程序维护的难度。
我们希望有这样一种处理机制:
if(用户输入的数据不合法){ ..... }else{ 处理逻辑 ..... }
上面伪码提供了一个非常强大的“if块”——程序不管输入错误的原因是什么,只要用户输入不满足要求,程序就一次处理所有的错误。这种处理方法的好处是,使得错误处理代码变得更有条理,只需在一个地方处理错误。
这就需要用到java异常了。
异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。
比如说,你的代码少了一个分号,那么运行出来结果是提示是错误java.lang.Error
;如果你用System.out.println(11/0)
,那么你是因为你用0做了除数,会抛出java.lang.ArithmeticException
的异常。
异常发生的原因有很多,通常包含以下几大类:
- 用户输入了非法数据。
- 要打开的文件不存在。
- 网络通信时连接中断,或者JVM内存溢出。
Java中的异常有以下三种类型:
检查异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
运行异常:运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
错误:错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。
异常的体系结构:
我们先来统观以下Java的异常体系结构图:
Java的异常被分为两大类:Checked异常和Runtime异常(运行时异常)。所有的RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。
只有Java语言提供了Checked异常,其他语言都没有提供Checked异常。Java认为Checked异常都是可以被处理(修复)的异常,所以Java程序必须显式处理Checked异常。如果程序没有处理Checked异常,该程序在编译时就会发生错误,无法通过编译。
Throwable:
Java异常的顶级类,所有的类都继承自Throwable
Error:
Error错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch 块来捕获Error对象。
在定义该方法时,也无须在其throws子句中声明该方法可能抛出Error及其任何子类。
Exception:
Exception中异常主要分为两大类,运行时异常和检查异常。常见的运行时异常有ArrayIndexOutOfBoundsException(数组下标越界)、NullPointerException(空指针异常)、ArithmeticException(算术异常)、ClassNotFoundException(类型找不到),这些异常时非检查异常,程序可以选择处理,也可以不处理,编译器编译时期并不会报错。这些异常一般是由于程序逻辑错误引起的,所以建议程序员还是处理一下。除运行时异常外的所有异常我们都称为非运行时异常,也是必须处理的异常,如果不出来,程序编译会报错。
Error和Exception的区别:
Error
和Exception
的区别:Error
通常是灾难性的致命的错误,是程序无法控制和处理的,当出现这些异常时,Java虚拟机(JVM)一般会选择终止线程;Exception
通常情况下是可以被程序处理的,并且在程序中应该尽可能的去处理这些异常。
检查异常:必须处理的异常
除了RuntimeException
及其子类以外,其他的Exception
类及其子类都属于检查异常,当程序中可能出现这类异常,要么使用try-catch
语句进行捕获,要么用throws
子句抛出,否则编译无法通过。
非检查异常:可以不处理
包括RuntimeException
及其子类和Error
。
不受检查异常
为编译器不要求强制处理的异常,检查异常
则是编译器要求必须处置的异常。
异常处理机制:
Java的异常处理机制可以让程序具有极好的容错性,让程序更加健壮。当程序运行出现意外情形时,系统会自动生成一个Exception对象来通知程序,从而实现将“业务功能实现代码”和“错误处理代码”分离,提供更好的可读性。
java异常关键字:
try
– 用于监听。try后紧跟一个花括号括起来的代码块(花括号不可省略),简称try块,它里面放置可能引发异常的代码,当try语句块内发生异常时,异常就被抛出。【监控区域】catch
– 用于捕获异常。catch后对应异常类型和一个代码块,用于处理try块发生对应类型的异常。【异常处理程序】finally
– 用于清理资源,finally语句块总是会被执行。 多个catch块后还可以跟一个finally块,finally块用于回收在try块里打开的物理资源(如数据库连接、网络连接和磁盘文件)。只有finally块执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。【使用finally进行清理】throw
– 用于抛出一个实际的异常。throw可以单独作为语句使用,抛出一个具体的异常对象。【抛出异常】throws
--用在方法签名中,用于声明该方法可能抛出的异常。【异常说明】
1、使用try...catch捕获异常:
语法格式:
try{ //业务实现代码 ... }catch(Exception e){ //异常处理代码 ... }
如果执行try块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给Java运行时环境,这个过程被称为抛出(throw)异常。
当Java运行时环境收到异常对象时,会寻找能处理该异常对象的catch块,如果找到合适的catch块,则把该异常对象交给该catch块处理,这个过程被称为捕获(catch)异常;如果Java运行时环境找不到捕获异常的catch块,则运行时环境终止Java程序也将退出。
下面看几个简单的异常捕获的例子:
例1:
public class DivTest { 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); }catch(IndexOutOfBoundsException e) { System.out.println("数组越界,运行时参数不够"); }catch(NumberFormatException e) { System.out.println("数字格式异常"); }catch(ArithmeticException e) { System.out.println("算术异常"); }catch(Exception e) { System.out.println("未知异常"); } } }
- 如果运行该程序时输入的参数不够,将会发生数组越界异常,Java运行时将调用IndexOutOfBoundsException对应的catch块处理该异常。
- 如果运行该程序时输入的参数不是数字,而是字母,将发生数字格式异常,Java运行时将调用NumberFormatException 对应的catch块处理该异常。
- 如果运行该程序时输入的第二个参数是0,将发生除0异常,Java运行时将调用ArithmeticException对应的catch块处理该异常。
- 如果程序运行时出现其他异常,该异常对象总是Exception类或其子类的实例,Java运行时将调用Exception对应的catch块处理该异常。
上面程序中的三种异常是我们在编程中经常遇见的,读者应该掌握这些异常。
例2:
import java.util.Date; public class Test { public static void main(String[] args) { Date d=null; try { System.out.println(d.after(new Date())); }catch(NullPointerException e) { System.out.println("空指针异常"); }catch(Exception e) { System.out.println("未知异常"); } } }
上面程序针对NullPointerException异常提供了专门的异常处理块。上面程序调用一个null对象的after0方法,这将引发NullPointerException异常(当试图调用一个null对象的实例方法或实例变量时,就会引发NullPointerException异常),Java 运行时将会调用NullPointerException对应的catch块来处理该异常;如果程序遇到其他异常,Java运行时将会调用最后的catch块来处理异常。
catch块处理异常遵循着:先小后大,即先子类后父类。正如在前面程序所看到的,程序总是把对应Exception类的catch块放在最后,这是为什么呢?读者可能明白原因:如果把Exception类对应的catch块排在其他catch块的前面,Java运行时将直接进入该catch块(因为所有的异常对象都是Exception或其子类的实例),而排在它后面的catch块将永远也不会获得执行的机会。
多异常捕获:
在Java7之前,每个catch块只能捕获一个异常,Java7之后,每个catch块可以捕获多种类型的异常。
上面的例1可以改成如下代码实现:
public class Test { 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); }catch(IndexOutOfBoundsException|NumberFormatException|ArithmeticException e) { System.out.println("数组越界,数字格式异常,算术异常"); }catch(Exception e) { System.out.println("未知异常"); } } }
2、使用throws声明抛出异常:
使用throws声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常将交给JVM处理。JVM对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行,这就是前面程序在遇到异常后自动结束的原因。
import java.io.FileInputStream; import java.io.FileNotFoundException; public class Test { public static void main(String[] args) throws FileNotFoundException { FileInputStream fis=new FileInputStream("a.txt"); } }
上面程序声明不处理IOException异常,将该异常交给JVM处理,所以程序一旦遇到该异常,JVM就会打印该异常的跟踪栈信息,并结束程序。运行上面程序,会看到如下图所示的运行结果。
3、使用throw抛出异常:
public class Test { public static void main(String[] args) throws Exception { try { int a=Integer.parseInt(args[0]); int b=Integer.parseInt(args[1]); int c=a/b; if(b==0) { throw new Exception("除数不能为0"); } System.out.println("您输入的两个数相除的结果是"+c); }catch(Exception e) { System.out.println("未知异常"); } } }
上面程序中粗体字代码使用throw语句来自行抛出异常。当Java运行时接收到开发者自行抛出的异常时,同样会中止当前的执行流,跳到该异常对应的catch块,由该catch块来处理该异常。也就是说,不管是系统自动抛出的异常,还是程序员手动抛出的异常,Java运行时环境对异常的处理没有任何差别。
4、访问异常信息:
如果程序需要在catch块中访问异常对象的相关信息,则可以通过访问catch块的后异常形参来获得。当Java运行时决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,程序即可通过该参数来获得异常的相关信息。
所有的异常对象都包括如下几个常用的方法:
getMessage():返回该异常信息的跟踪栈信息输出到标准错误输出
printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。
printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。
getStackTrace():返回该异常的跟踪栈信息。
5、使用finally回收资源:
有些时候,程序在try块里打开了一些物理资源(例如数据库连接、网络连接和磁盘文件),这些物理资源都必须显示回收。
在哪里回收这些物理资源呢?在try块里回收?还是在catch块中进行回收?假设程序在try块里进行资源回收,根据图10.1所示的异常捕获流程—一如果try块的某条语句引起了异常,该语句后的其他语句通常不会获得执行的机会,这将导致位于该语句之后的资源回收语句得不到执行。如果在catch块里进行资源回收,但catch块完全有可能得不到执行,这将导致不能及时回收这些物理资源。
为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。完整的Java异常处理语法结构如下:
try{ //业务实现代码 ... }catch(SubException e){ //异常处理块 ... }catch(SubException e2){ //异常处理块 ... }finally{ //资源回收 ... }
例如:
public class Test { public static void main(String[] args) throws Exception { try { int a=10; int b=0; int c=a/b; }catch(Exception e) { System.out.println("未知异常"); }finally { System.out.println("资源回收"); } } }
结果:
未知异常 资源回收
总结:
以上是关于Java基础系列5:深入理解Java异常体系的主要内容,如果未能解决你的问题,请参考以下文章