异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。
Thorwable类是所有异常和错误的超类,有两个子类Error和Exception,分别表示错误和异常。其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常,这两种异常有很大的区别,也称之为不检查异常(Unchecked Exception)和检查异常(Checked Exception)。
运行时异常和非运行时异常:
运行时异常都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
非运行时异常是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、ClassNotFoundException等以及用户自定义的异常。
程序中有时会出现特有的异常,而这些异常并未被java所描述并封装对象,对于这些特有的异常可以按照java的封装思想。将特有的异常按照Java异常机制进行自定义的异常封装。
异常体系具备一个特有的特性:可抛性
:可以被throw关键字操作。自定义异常被抛出,必须是继承Throwable,或者继承Throwable的子类,该对象才可以被throw抛出。
一般自定义异常继承Exception或者RuntimeException,选择哪种继承取决于是否需要对异常进行捕获处理。
在异常中往往需要封装异常信息,查阅异常源码,发现自己父类构造函数中有关于异常信息的操作,那么在自己定义的异常中需要将这些信息传递给父类,让父类帮我们进行封装即可。
class NoAgeException extends RuntimeException{
/*
为什么要定义构造函数,因为看到Java中的异常描述类中有提供对问题对象的初始化方法。
*/
NoAgeException(){
super();
}
NoAgeException(String message) {
super(message);// 如果自定义异常需要异常信息,可以通过调用父类的带有字符串参数的构造函数即可。
}
}
异常处理的两大组成要素:抛出异常和捕获异常。这两大要素共同实现程序控制流的非正常转移。
抛出异常分为:显式和隐式两种。
显式抛异常的主题是应用程序,它指的是在程序中使用 “throw” 关键字。手动将异常实例抛出。
隐式抛异常的主题是java虚拟机,它指的是java虚拟机在执行过程中,碰到无法继续执行的异常状态,自动过抛出异常。举例来说,java虚拟机在执行读取数组操作时,发现输入的索引值是负数,故而抛出数组索引越界异常(ArrayIndexOutOfBoundsException)。
捕获异常则涉及了如下三种代码块:
1、try代码块:用来标记需要进行异常监控的代码。
2、catch代码块:跟在try代码块之后,用来捕获在try代码块中触发的某种类型的异常。除了声明所捕获异常的类型之外,catch代码块还定义了针对该异常类型的异常处理器。在java中try代码块后可以跟多个catch代码块,来捕获不同的异常。java虚拟机会从上至下匹配异常处理器。因此,前面的catch代码块所捕获的异常类型不能覆盖后面的,否则编译器会报错。
3、finally代码块:跟在try代码块和catch代码块之后,用来声明一段必定运行的代码。它的设计初衷是为了避免跳过某些关键的清理代码。例如关闭已打开的系统资源。
在程序正常执行的情况下,这段代码会在try代码块执行之后执行。否则,也就是在try代码块抛异常的情况下,如果该异常没有被捕获,finally代码块会直接运行,并且在运行之后重新抛出异常。
如果该异常被catch代码块捕获,finally代码块则在catch代码块之后运行。在某些不幸的情况下,catch代码块也触发了异常,那么finally代码块同样会执行,并会抛出catch代码块触发的异常。在某极端不幸的情况下,finally代码块也触发了异常,那么只好中断当前finally代码块的执行,并往外抛出异常。
捕获格式:
try
{
//需要被检测的语句。
}
catch(异常类 变量)//参数。
{
//异常的处理语句。
}
finally
{
//一定会被执行的语句。
}
class Demo{
/*
如果定义功能时有问题发生需要报告给调用者。可以通过在函数上使用throws关键字进行声明。
*/
void show(int x)throws Exception {
if(x>0){
throw new Exception();
}else{
System.out.println("show run");
}
}
}
class ExceptionDemo{
public static void main(String[] args)//throws Exception//在调用者上继续声明。
{
Demo d = new Demo();
try {
d.show(1);//当调用了声明异常的方法时,必须有处理方式。要么捕获,要么声明。
}
catch (Exception ex)//括号中需要定义什么呢?对方抛出的是什么问题,在括号中就定义什么问题的引用。
{
System.out.println("异常发生了");
}
System.out.println("Hello World!");
}
}
描述长方形。
考虑健壮性问题。万一长和宽的数值非法。描述问题,将问题封装成对象,用异常的方式来表示。
class NoValueException extends RuntimeException{
NoValueException() {
super();
}
NoValueException(String message) {
super(message);
}
}
class Rec{
private int length;
private int width;
Rec(int length,int width) {
if(length<=0 ||width<=0) {
//抛出异常,但是不用声明,不需要调用者处理。就需要一旦问题发生让调用者端停止,让其修改代码。
throw new NoValueException("长或者宽的数值非法");
}
this.length = length;
this.width = width;
}
/**
定义面积函数。
*/
public int getArea() {
return length*width;
}
}
class ExceptionTest{
public static void main(String[] args) {
Rec r = new Rec(-3,4);
int area = r.getArea();
System.out.println("area="+area);
}
}
在编译生成的字节码中,每个方法都附带一个异常表。异常表中,每一个条目代表一个异常处理器,并且由from指针、to指针和target指针以及所捕获的异常类型构成。这些指针的值是字节码索引(bytecode index,bci)用以定位字节码。其中from指针和to指针标示了该异常处理器所监控的范围,例如try代码块所覆盖的范围。target指针则指向异常处理器的起始位置,例如catch代码块的起始位置。
当程序触发异常时,JVM会从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,JVM会判断所抛出的异常和该条目想要捕获的异常是否匹配。如果匹配,JVM会将控制流转移至该条目target指针指向的字节码。如果遍历完所有的条目,JVM仍未匹配到异常处理器,那么它会弹出当前方法所对应的java栈帧,并且在调用者(caller)中重复上述操作。在最坏的情况下,JVM需要遍历当前线程java栈上所有方法的异常表。
finally代码块的编译比较复杂。当前版本java编译器的做法,是复制finally代码块的内容,分别放在try-catch代码块所有正常执行路径以及异常执行路径的出口中。针对异常执行路径,java编译器会生成一个或多个异常表条目,监控整个try-catch代码块,并且捕获所有种类的异常。这些异常表条目的target指针将指向将指向另一份复制的finally代码块。并且,在这个finally代码块的最后,java编译器会重新抛出所捕获得异常。