Java 异常解析
前言
异常处理使得程序可以处理非预期的情景,并且继续正常的处理。
在程序运行过程中,如果 JVM 检测出一个不可能执行的操作,就会出现运行时错误(runtime error)。例如,如果使用一个越界的下标访问数组,程序就会产生一个ArraylndexOutOfBoundsException 的运行时错误。如果程序需要输入一个double 值,会得到一个 InputMismatchException 的运行时错误。
在 Java 中,运行时错误会作为异常抛出。异常就是一种对象,表示阻止正常进行程序执行的错误或者情况。如果异常没有被处理,那么程序将会非正常终止。该如何处理这个异常,以使程序可以继续运行或者优雅终止呢?本篇文章将介绍该主题。
一、异常概述
异常就是程序在运行过程中出现的一些错误,我们通过面向对象的思想,把这些错误也用类来描述,那么一旦产生一个错误,即就是创建了某一个错误的对象,这个对象就是我们所说的异常对象。
在这之前也见过很多的异常错误,例如:
(1)IndexOutOfBoundsException:
-
ArrayIndexOutOfBoundsException
数组角标越界异常 角标不在数组范围内 -
StringfIndexOutOfBoundsException
字符串角标越界异常 角标不在字符串范围内
(2)NullPointerException
空指针异常 对null调用其成员。
(3)ArithmeticException
数学运算异常 非法的数学运算。
(4)ClassCastException
类型转换异常 将类进行错误的强制转换。
(5)NumberFormatException
数字格式化异常 将数字字符串进行解析。
(6)InputMismatchException
输入不匹配异常 在输入时输入非法数据。
(7)ParseException
时间解析异常 非法的时间格式。
(8)StackOverFlowError
栈内存溢出异常 函数递归。
(9)OutOfMemoryError
堆内存异常 数组空间开辟过大 程序中对象太多。
为了演示异常处理,在这里举了个例子,进行讲解:
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
int num = getNumber(arr,10);
System.out.println(num);
}
private static int getNumber(int[] arr, int i) {
return arr[i];
//如果arr为null NullPointerException
//如果i越界 ArrayIndexOutOfBoundsException
}
如上述代码所示,对于一个有返回值的函数而言,如果操作计算是正确的话,肯定会有一个正常的返回值,如果操作计算是错误的话,从函数的角度而言 会不会有正常的返回值呢?不会;
本质上,大部分的错误,其实都是由JVM发现并抛出的 当然我们也可以手动去判断。
private static int getNumber(int[] arr, int i) {
//return arr[i];//是由JVM来进行判断的
//模拟JVM的操作 用手动的判断模拟JVM自动的发现
if (arr == null) {
//产生一个空指针异常的问题对象
//用throw(抛出)关键 将产生的问题告知调用者
throw new NullPointerException();
//一旦上述抛出一个问题 此函数立马中断
//类似于return 正常结束(弹栈)
//throw非正常结束(中断 强制弹栈 并抛出问题给调用者)
}
if (i < 0 || i >= arr.length) {
//产生一个数组角标越界异常的问题对象
throw new ArrayIndexOutOfBoundsException();
//throw new StackOverflowError();
//PS 一旦出现错误 抛出问题即可 但是最好抛出最相关最精确的问题
}
return arr[i];
}
getNumber函数如果发生了问题是直接将问题抛给主函数的,但是主函数也没有处理这个问题,主函数接着将问题抛给调用者JVM,JVM才不帮你解决 结果就是程序中断了!
JVM----->调用main----->调用getNumber----->getNumber出现异常----->抛出给main----->main没有处理----->抛出传递给JVM----->JVM直接中断。
PS:当然 函数内部如果出现问题,也可以将问题在内部处理 处理后 就不需要向上层抛出。
二、异常类型
既然错误的原因有很多,描述错误的类也有很多,那么这种情况就产生了一个大的家族/体系,这里面全都是异常错误类,我们叫做异常体系。
对于异常体系而言,大家都是错误,只不过所产生的原因不太一样,不断的进行向上抽取共性,最终抽出Throwable这个接口(可以被抛出的),但凡是异常对象,无论原因,都可以被抛出。
Throwable类是 Java 语言中所有错误或异常的超类,只有当对象是此类(或其子类之一)的实例时,才能通过 Java 虚拟机或者 Java throw 语句抛出。
异常是对象,而对象都采用类来定义。异常的根类是 java.lang.Throwable。关系图如下:
注意:类名 Error、Exception 和 RuntimeException 有时候容易引起混淆。这三种类都是异常,这里讨论的错误都发生在运行时。
Throwable 类是所有异常类的根。所有的 Java 异常类都直接或者间接地继承自Throwable。可以通过继承 Exception 或者 Exception 的子类来创建自己的异常类。
这些异常类可以分为三种主要类型:系统错误、编译时异常和运行时异常。
1.系统错误(Error)
- 系统錯误(system error) 是由 Java 虚拟机抛出的,用 Error 类表示。Error 类描述的是内部系统错误。这样的错误很少发生。如果发生,除了通知用户以及尽量稳妥地终止程序外,几乎什么也不能做。
OutOfMemoryError :内存耗尽 ;
NoClassDefFoundError :无法加载某个Class ;
StackOverflowError :栈溢出。
2.编译时异常(Exception)
- 编译时异常:Exception及其子类(除了RuntimeException),在编译时期抛出的异常,在编译期间检查程序是否可能会出现问题,如果可能会有,则预先防范:捕获 声明。
Exception 则是编译时异常,它可以被捕获并处理。
某些异常是应用程序逻辑处理的一部分,应该捕获并处理。例如:
- NumberFormatException :数值类型的格式错误;
- FileNotFoundException :未找到文件;
- SocketException :读取网络失败。
还有一些异常是程序逻辑编写不对造成的,应该修复程序本身。例如:
- NullPointerException :对某个 null 的对象调用方法或字段;
- IndexOutOfBoundsException :数组索引越界。
3.运行时异常(RuntimeException)
- 运行时异常(runtime exception) 是用 RuntimeException 类表示的,它描述的是程序设计错误,例如,错误的类型转换、访问一个越界数组或数值错误。运行时异常通常是由 Java 虚拟机抛出的。
- RuntimeException是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类,可能在执行方法期间抛出但未被捕获的 RuntimeException 的任何子类都无需在 throws 子句中进行声明,指的就是这些问题不需要提前被预防(本质上也可以的,只不过没必要),因为只有在真正运行的时候才能发现是否发生问题,一旦在运行期间发生了问题,则一般不会修正,程序直接终端。
RuntimeException、Error 以及它们的子类都称为免检异(unchecked exception )。所有其他异常(编译时异常)都称为必检异常(checked exception), 意思是指编译器会强制程序员检査并通过 try- catch 块处理它们,或者在方法头进行声明。
在大多数情况下,免检异常都会反映出程序设计上不可恢复的逻辑错误。例如,如果通过一个引用变量访问一个对象之前并未将一个对象陚值给它,就会抛出 NullPointerException异常;如果访问一个数组的越界元素,就会抛出IndexOutOfBoundsException 异常。这些都是程序中必须纠正的逻辑错误。免检异常可能在程序的任何一个地方出现。为避免过多地使用 try-catch 块,Java 语言不强制要求编写代码捕获或声明免检异常。
Java规定:
- 必须捕获的异常,包括 Exception 及其子类,但不包括 RuntimeException 及其子类,这种类型的异常称为Checked Exception。
- 不需要捕获的异常,包括 Error 及其子类, RuntimeException 及其子类。
三、处理编译时异常的更多知识
1. 声明异常
在 Java 中,当前执行的语句必属于某个方法。Java 解释器调用 main 方法开始执行一个程序。每个方法都必须声明它可能抛出的必检异常的类型。这称为声明异常( declaring exception)。只对编译时异常进行声明,、告知方法的调用者有异常。
为了在方法中声明一个异常,就要在方法头中使用关键字 throws, 如下所示:
public void myMethodO throws IOException
关键字 throws 表明 myMethod 方法可齙会抛出异常 IOException。如果方法可能会抛出多个异常,就可以在关键字 throws 后添加一个用逗号分隔的异常列表:
public void myMethodO throws Exceptionl, Exception2,… ,ExceptionN
注意:如果方法没有在父类中声明异常,那么就不能在子类中对其进行继承来声明异常。
2. 抛出异常
检测到错误的程序可以创建一个合适的异常类型的实例并抛出它,这就称为抛出一个异常(throwing an exception)。这里有一个例子,假如程序发现传递给方法的参数与方法的合约不符(例如,方法中的参数必须是非负的,但是传入的是一个负参数),这个程序就可以创建 IllegalArgumentException 的一个实例并抛出它,如下所示:
IllegalArgumentException ex = new II1egalArgumentException("Wrong Argument");
throw ex;
或者,大多数使用下面的语句:
throw new IllegalArgumentException("Wrong Argument");
注意:IllegalArgumentException 是 Java API 中的一个异常类。通常,JavaAPI 中的每个异常类至少有两个构造方法:一个无参构造方法和一个带可描述这个异常的 String 参数的构造方法。该参数称为异常消息(exceptionmessage), 它可以用 getMessage()获取。
提示:声明异常的关楗字是 throws, 抛出异常的关键字是 throw。
3. 捕获异常
现在我们知道了如何声明一个异常以及如何抛出一个异常。当抛出一个异常时,可以在try-catch 块中捕获和处理它;
try-catch-finally
try语句块中:放的是可能出现问题的代码,尽量不要把不相关的代码放入到里面,否则会出现截断的问题。
try{
codeA
throw ...
codeB
}
如果throw这个地方一旦抛出异常 codeB就不会执行了 建议把codeB放到后面。
catch语句块中:放的是出现问题并捕获后,处理问题的代码code,如果问题在try语句块中没有出现 则catch中不会运行。
catch(Exception e) {
code
}
finally语句块中:放的是不管问题异常是否产生 都要执行的代码code。
finally{
code//关闭资源(IO 数据库 网络),结尾处理的一些工作
}