细节!关于java异常的总结,我还没见过比这更详细的

553 阅读7分钟

前言

异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。

异常的体系结构:

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);
	}
}

JVM的捕获异常机制

在编译生成的字节码中,每个方法都附带一个异常表。异常表中,每一个条目代表一个异常处理器,并且由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编译器会重新抛出所捕获得异常。

最后

感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!