2025年深入理解Java异常机制

深入理解Java异常机制java 中的异常处理的目的在于通过使用少量的代码 使得程序有着强大的鲁棒性 并且这种异常处理机制会让你变得非常自信 你的应用中没有你没处理过的错误 处理异常的相关手法看起来是这么的难懂 但是如果掌握的话 会让你的项目收益明显 效果也会是立竿见影 如果我们不使用异常处理

大家好,我是讯享网,很高兴认识大家。

(一)解决异常情形的基本思路


1.普通问题和异常情形
异常情形是指阻止当前方法或作用域继续执行的问题。我们首先需要区分普通问题和异常情形,普通问题是在编程的过程中,我们可以通过已知的信息解决,并继续执行的问题。异常情形是指在当前的情况下,我们不能继续下去了,在当前的环境下我们不能解决这个问题,我们只能将这个问题抛出当前的环境,到一个大的环境中去企图解决这个问题,这就是抛出异常时发生的事情。

2.抛出异常之后
我们在区分了普通问题和异常之后说到,如果我们没有能力处理的问题就需要抛出异常。在抛出异常的时候,会有几件事情发生:

java用new在堆上来创建一个异常对象。 当前执行的程序不能被执行下去,程序被终止之后,从当前环境弹出一个对异常对象的引用。 异常处理机制接管程序,试图找到一个恰当的地方来执行异常处理。

讯享网

异常处理使得我们可以将每件事情都看作一个事物来处理,而异常可以看作这些事务的底线。我们还可以将异常看作是一种内建的恢复系统,因为我们的程序中可以拥有各种不同的恢复点。如果程序的某部分事物都失败了,我们至少可以返回一个稳定的安全处。

3.两种异常处理的基本模型
在所有现存的语言中,对于处理异常有两种基本模型。 
java支持的模型是终止模型,(c++,c,python,c#也是如此)。在这种模型中,假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行程序。一旦异常被抛出,说明程序已经无法挽回,不可能回到原处继续进行了。 
另外一种称为恢复模型,这种模型认为异常处理程序的工作是修正错误,然后重新尝试调用出问题的地方,并认为能够二次成功。

虽然恢复模型开始显得很吸引人,但不是很实用。其中的主要原因可能是它所导致的耦合,因为恢复性的处理程序需要了解异常抛出的地点,这一点是致命的。我们怎么才能告知程序我的代码在哪里出错呢?这势必要包含了非通用性的高耦合代码,这增加了编写和维护的难度。

(二)捕获异常

1.将异常对象看作“返回”
关键词throw会产生很多有趣的结果。在使用new创建了异常对象之后,此对象的引用将会传递给throw。尽管返回异常对象其类型通常与方法设计的返回类型不同,但从效果上看,我们可以假装认为从这个方法或代码块“返回”了一个异常对象给throw。

另外,我们可以抛出任意类型的Throwable对象,他是异常类的根类(祖先类)。错误信息可以保存在异常对象内部或者用异常类的名称来表示。

2.try块
如果代码中抛出异常,那么我们的程序将会终止,如果不希望程序就此结束,我们可以通过try-catch块来操作。在try块中的内容如果抛出了异常,我们只会结束try块中运行的内容,而不会结束整个程序。

3.catch块
catch块就是我们刚才一直提到的异常处理程序。catch块在try块之后,我相信try-catch大家都知道,这里也就不把这种基本的东西介绍个没完了。需要注意的是,在try块内部,可能有几个方法都会抛出同一个异常,我们只需写一个这种异常的catch块就可以捕获所以,无需重复书写。而且,catch块要按照从细小到广泛的顺序来写,如果我们把Exception放在第一个,那么剩下的具体的异常catch块将捕获不到异常,因为Exception可以处理所有的。

讯享网catch(FileNotFoundException | IOException e) { //如果这两个异常的操作是一样的,我们可以把他们的操作写在一起,从jdk7开始 }

4.创建一个自己定义的异常类
我们先创建一个自己定义的异常类,我们可以看到,异常类有这样的两种构造器方法,一种是默认的无参数构造方法,另外一种是传递一个String类型的出错原因的构造器。(异常一共有4种构造器方法,这里只说了两种)我们什么都不用做,因为我们现在只是简单的看一下,具体的内容我们以后再来添加。

package ExceptionEx; / * * @author QuinnNorris * * 自定义异常类 */ public class MyException extends Exception { public MyException() { } public MyException(String msg) { super(msg); } } 

之后,我们要创建一个测试类,在这个测试类中我们会让一个方法手动的抛出一个异常。这种方法只是为了演示,在实际的情况下,异常对象地抛出都是因为我们的编译问题由编译器为我们抛出的。在我们用方法抛出一个异常后,我们会用try-catch块来自己接住这个异常。

讯享网package ExceptionEx; / * * @author QuinnNorris * * 测试类,用方法抛出异常 */ public class TestExcep { / * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub try { throwMyExce(); } catch (MyException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public static void throwMyExce() throws MyException { throw new MyException(); } } 

输出的结果是我们出错的栈轨迹,之所以会输出这些内容,是我们在catch块中调用printStackTrace方法的结果。

输出结果: ExceptionEx.MyException at ExceptionEx.TestExcep.throwMyExce(TestExcep.java:20) at ExceptionEx.TestExcep.main(TestExcep.java:12) 

 

(三)异常类继承关系树


1.异常类关系图
 javaå¼å¸¸ç±»å³ç³»æ 
讯享网
看一张图,一看就懂。

2.Exception和Error类
在java中,所有的异常都有一个共同的祖先 Throwable类。而Throwable类有两个子类,一个是Error类,一个是Exception,这两个哥俩分别管两种不同类型的错误。

 注:如果你非要用try-catch块来包含自己的代码免受RunTimeException的困扰,从理论上编译器是不会报错的。但是这样只会让你的代码变得冗长,而且你会因此而不知道你的逻辑到底有没有问题。这是一种非常不好的行为。

一句话来总结:运行异常是程序逻辑错误,无法预料,改正就可以,无需抛出或处理。非运行时异常是显然可能存在的错误,我们被强制必须抛出或处理来保证程序的安全性。

3.已检查的异常、未检查的异常
通常,Java的异常(Throwable)又可以被分为已检查异常(checked exceptions)和未检查异常(unchecked exceptions)这两种。其实这两种和我们上面的差不多,我们在这里给出一下概念:

(四)Execption类

1.getMessage、getLocalizedMessage
我们刚才说过,Exception类有这样两种构造器(异常类一共四种构造器),有参数的构造器有一个String的参数,是用来存储错误信息的,那么既然能够存储信息,也肯定有读取信息的方法。
 

讯享网public String getMessage() //返回此 throwable 的详细消息字符串。 public String getLocalizedMessage() //创建此 throwable 的本地化描述。子类可以重写此方法,以便生成特定于语言环境的消息。 //对于不重写此方法的子类,默认实现返回与 getMessage() 相同的结果。 

我们通过这两种方法来获得存储着信息的字符串。

2.printStackTrace

printStackTrace这个方法有三种重载的形态,总的来说,这个方法的作用是输出错误信息。

 

public void printStackTrace() //将此 throwable 及其追踪输出至标准错误流。 //此方法将此 Throwable 对象的堆栈跟踪输出至错误输出流,作为字段 System.err 的值。 //输出的第一行包含此对象的 toString() 方法的结果。 //剩余行表示以前由方法 fillInStackTrace() 记录的数据。 public void printStackTrace(PrintStream s) //将此 throwable 及其追踪输出到指定的输出流。 public void printStackTrace(PrintWriter s) //将此 throwable 及其追踪输出到指定的 PrintWriter。 

我们给出一个API中的例子:

讯享网 class MyClass { public static void main(String[] args) { crunch(null); } static void crunch(int[] a) { mash(a); } static void mash(int[] b) { System.out.println(b[0]); } } 

 输出b[0]的时候,这个数组是不存在的,肯定是一个空指针的错误。通过上面的这个这个例子会产生以下的错误。

 java.lang.NullPointerException at MyClass.mash(MyClass.java:9) at MyClass.crunch(MyClass.java:6) at MyClass.main(MyClass.java:3)

3.fillInStackTrace

在上一个printStackTrace显示栈轨迹的方法中有说过,栈轨迹是存储在这个方法中的

讯享网public Throwable fillInStackTrace() //在异常堆栈跟踪中填充。此方法在 Throwable 对象信息中记录有关当前线程堆栈帧的当前状态。 

 

4.getStackTrace、setStackTrace

我们还有以数组的形式获得和修改栈轨迹的方法,但是在一般情况下,我们几乎不会用到,这里就展示一下

public StackTraceElement[] getStackTrace() //提供编程访问由 printStackTrace() 输出的堆栈跟踪信息。 //返回堆栈跟踪元素的数组,每个元素表示一个堆栈帧。 //数组的第零个元素(假定数据的长度为非零)表示堆栈顶部,它是序列中最后的方法调用。 public void setStackTrace(StackTraceElement[] stackTrace) //设置将由 getStackTrace() 返回,并由 printStackTrace() 和相关方法输出的堆栈跟踪元素。 //此方法设计用于 RPC 框架和其他高级系统,允许客户端重写默认堆栈跟踪,

(五)如何捕获全部异常?

1.栈轨迹
栈轨迹是一种概念。我们的异常存放于一个异常堆栈中,这个栈有栈顶,和一帧帧的位置来保存异常被抛出的路径。

讯享网 java.lang.NullPointerException at MyClass.mash(MyClass.java:9) at MyClass.crunch(MyClass.java:6) at MyClass.main(MyClass.java:3)

这是printStackTrace方法打印出来的栈轨迹,这个方法返回一个由栈轨迹中的元素所构成的数组。每个元素表示栈中的一个帧。数组中的最后一个元素也是栈底也是调用序列中离抛出异常最远的第一个方法调用。

2.重抛异常
在很多个方法递归调用的情况下,有的时候我们用try-catch块接住了异常,但是我们并不想在这个方法中处理,而是想把它继续向上抛出,到上一个块中处理。那么这个时候我们就需要重新抛出异常。

重抛异常会把异常抛给上一级环境中的异常处理程序,同一个try块的后续catch子句将会被忽略。此外,异常对象的所有信息都得以保持。所以高一级的环境中捕获此异常的处理程序可以从这个异常对象中得到所有信息。

需要注意的是,如果我们多次重抛异常,在这之后我们再调用printStackTrace()方法,现实的栈轨迹是全部的轨迹,而不是最后一次抛出时开始的轨迹,这是我们希望的情况。同时,还有另外一件事,fillInStackTrace()方法会返回一个异常对象,会刷新当前的栈轨迹,也就是说我们调用fillInStackTrace的话,那一行就变成了异常的新发生地了。那么这两件事情结合在一起,我们得出了一个结论:多次重抛同一个异常,不会调用fillInStackTrace方法刷新栈轨迹。但是还有另外的情况,就是我们在捕获异常之后抛出了另一个异常。因为这样做会替换那个异常对象的堆栈,就相当于使用了fillInStackTrace()方法,重抛了不同的异常,会导致有关原来的异常发生点的信息会丢失,剩下的是新的异常的抛出点的信息。

package Test; / * * @author QuinnNorris * * 重抛异常测试类 */ public class Test{ public Test() { } void testEx() throws Exception { try { testEx1(); } catch (Exception e) { throw e; } } void testEx1() throws Exception { try { testEx2(); } catch (Exception e) { //throw e; //throw new Exception(); } } void testEx2() throws Exception { try { int b = 1; int c; for (int i = 2;; i--) { c = b / i; } } catch (Exception e) { throw e; } } public static void main(String[] args) throws Exception { Test testException1 = new Test(); testException1.testEx(); } } 

这个代码模拟了我们平时工作的方法多层调用的情况。

讯享网throw e;

 我们先把这个代码中注释掉的第一行去掉注释。

输出结果: Exception in thread “main” java.lang.ArithmeticException: / by zero at Test.Test.testEx2(Test.java:35) at Test.Test.testEx1(Test.java:23) at Test.Test.testEx(Test.java:15) at Test.Test.main(Test.java:44) 

这个时候,它的栈轨迹是这样的,这个是完全的从除零错误开始的站轨迹

讯享网throw new Exception();

这个时候我们再一次注释掉第一行,我们把第二行的注释去掉。

输出结果:Exception in thread “main” java.lang.Exception at Test.Test.testEx1(Test.java:26) at Test.Test.testEx(Test.java:15) at Test.Test.main(Test.java:44) 

完全变成了另外一个异常。这说明了这种重抛异常的时候可能发生的问题,可能上面的例子确实很简单,但是实际中,当我们许多个方法一调用可能就会被这种情况搞糊涂了。

3.异常链
常常会想要在捕获一个异常后跑出另一个异常,并且希望能将原来的异常信息保存下来,这种我们称之为异常链。在Throwable的子类构造器中都有一个可以接受一个叫做cause的Throwable对象作为参数,表述原始的那个异常。但是这个构造器只有几个异常类(Throwable,Error,Exception,RuntimeException)拥有,更普遍的写法是调用initCause方法。
 

讯享网public Throwable(Throwable cause) //构造一个带指定 cause 和 (cause==null ? null :cause.toString())的详细消息的新 throwable。 //此构造方法对于那些与其他 throwable(例如,PrivilegedActionException)的包装器相同的 throwable 来说是有用的。 //调用 fillInStackTrace() 方法来初始化新创建的 throwable 中的堆栈跟踪数据。 public Throwable(String message,Throwable cause) //构造一个带指定详细消息和 cause 的新 throwable。 //注意,与 cause 相关的详细消息不是 自动合并到这个 throwable 的详细消息中的。 //调用 fillInStackTrace() 方法来初始化新创建的 throwable 中的堆栈跟踪数据。 public Throwable initCause(Throwable cause) //将此 throwable 的 cause 初始化为指定值。(该 Cause 是导致抛出此 throwable 的throwable。) //此方法至多可以调用一次。此方法通常从构造方法中调用,或者在创建 throwable 后立即调用。 //如果此 throwable 通过 Throwable(Throwable) 或 Throwable(String,Throwable) 创建,此方法不能调用。 
package ExceptionEx; / * * @author QuinnNorris * * 继承了Exception的自定义异常类 */ class DynamicFieldException extends Exception { } / * * @author QuinnNorris * * 包含一个对象数组以及一些操作的主类 */ public class DynamicFields { private Object[][] fields; / * 用传入的大小参数设定初始化数组的行数 * * @param initialSize */ public DynamicFields(int initialSize) { fields = new Object[initialSize][2]; for (int i = 0; i < initialSize; i++) fields[i] = new Object[] { null, null }; } / * 重写toString,以为了能够输出数组 */ public String toString() { StringBuilder result = new StringBuilder(); for (Object[] field : fields) { result.append(field[0]); result.append(": "); result.append(field[1]); result.append("\n"); } return result.toString(); } / * 查询是否有是id的field[n][0] * * @param id * @return */ private int hasField(String id) { for (int i = 0; i < fields.length; i++) if (id.equals(fields[i][0])) return i; return -1; } / * 查询id的索引,如果有就返回索引,如果没有就抛出异常 * * @param id * @return * @throws NoSuchFieldException */ private int getFieldNumber(String id) throws NoSuchFieldException { int idIndex = hasField(id); if (idIndex == -1) throw new NoSuchFieldException(); return idIndex; } / * 查看全部的fields,将一个新的id添加进去。如果没有空位置就将数组添加一行返回索引。 * * @param id * @return */ private int makeField(String id) { for (int i = 0; i < fields.length; i++) if (fields[i][0] == null) { fields[i][0] = id; return i; } Object[][] tmp = new Object[fields.length + 1][2]; for (int i = 0; i < fields.length; i++) tmp[i] = fields[i]; for (int i = fields.length; i < tmp.length; i++) tmp[i] = new Object[] { null, null }; fields = tmp; return makeField(id); } / * 获取id的那一行储存的值 * * @param id * @return * @throws NoSuchFieldException */ public Object getField(String id) throws NoSuchFieldException { return fields[getFieldNumber(id)][1]; } / * 从id中取出值 * * @param id * @param value * @return * @throws DynamicFieldException */ public Object setField(String id, Object value) throws DynamicFieldException { if (value == null) { DynamicFieldException dfe = new DynamicFieldException(); dfe.initCause(new NullPointerException()); throw dfe; } int fieldNumber = hasField(id); if (fieldNumber == -1) fieldNumber = makeField(id); Object result = null; try { result = getField(id); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } fields[fieldNumber][1] = value; return result; } / * @param args */ public static void main(String[] args) { DynamicFields df = new DynamicFields(3); System.out.println(df); try { df.setField("d", "A value for d"); df.setField("number", 47); df.setField("number2", 48); System.out.println(df); df.setField("d", "A new value for d"); df.setField("number3", 11); System.out.println("df: " + df); Object field = df.setField("d", null); } catch (DynamicFieldException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 

每个对象都含有一个数组,元素是”成对的对象“。第一个对象表示字段标识符,第二个标识字段值,值的类型可以是除基本类型外的任意类型。当调用setField方法的时候,她将试图通过标识修改已有字段值,否则就建一个新的字段,并把值放入。如果空间不够了,将建立一个更长的数组,并把原来的数组的元素复制进去。如果你试图为字段设置一个空值,将抛出一个DynamicFieldException异常,它是通过使用initCause方法把NullPointerException对象插入而建立的。

讯享网public Object setField(String id, Object value) throws DynamicFieldException { if (value == null) { DynamicFieldException dfe = new DynamicFieldException(); dfe.initCause(new NullPointerException()); throw dfe; } int fieldNumber = hasField(id); if (fieldNumber == -1) fieldNumber = makeField(id); Object result = null; try { result = getField(id); } catch (NoSuchFieldException e) { throw new RuntimeException(e); }

或许你会为上面这段代码感到疑惑,为什么我们不直接在方法名后面声明NoSuchFieldException异常,而是捕获这个异常之后,将他变成RuntimeException。原因是这样的:如果在一个方法中发生了一个已检查异常,而不允许抛出它,那么我们可以捕获这个已检查异常,并将它包装成一个运行时异常。这个方法在这里看来有些多余,但是这种将已检查异常包装在未检查异常里的手法非常实用,在后面我们会介绍到,如果我们继承的方法没有声明抛出异常,我们是不能抛出异常的,到那个时候,我们会需要到这种手法。

(六)使用finally进行清理


1.finally作用
对于一些代码,可能会希望无论try块中的异常是否抛出,它们都能得到执行。为了达到这个效果,可以在异常处理程序后面加上finally子句。对于没有垃圾回收和析构函数自动调用机制的语言来说,finally非常重要。它能使程序员保证:无论try块中发生什么,内存总能得到释放。

但是在java中有垃圾回收机制,所以内存释放不再是问题。所以,更多的时候,当要把除了内存以外的资源恢复到它们的初始状态时,就要用到finally子句。这种资源包括:已经打开的文件或网络资源,在频幕上画的图形等等。稍微有一些项目经验的朋友就知道finally到底多么好用。

2.带资源的try块
在很多的情况下,我们用finally只是为了简单的将资源关闭。再注意到这种情况后,java se7为这种情况提供了一个很有用的快捷方式。可以为我们快速简洁的关闭用.close方法关闭的资源。
 

try(Resource res = ...) { //do some work }

在这种写法之下,try快退出时,会自动调用res.close()方法。但是这种写法仅仅在是“close”方法的时候可用,比如多线程中的ReentrantLock的关闭方式不是调用close方法,那这就不适用。

3.finally的异常丢失问题
遗憾的是,java中的异常实现有一些瑕疵。异常作为程序出错的标志,绝对不应该被忽略,如果被忽略会给调试者带来很大的麻烦。但是,请考虑这种情况:在try中调用了方法,这个方法抛出一个一场,但是在finally中又调用了其他的一个方法,这个新方法也抛出一个异常。理论上,当代码遇到异常的时候,会直接停止现在的工作,抛出异常,但是finally这种特殊的机制,导致又抛出了一个异常,而且这种抛出直接导致前面的异常被覆盖了。

讯享网package ExceptionEx; public class FinallyEx { / * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub try { throw new RuntimeException(); } finally{ return; } } } 

4.finally块中的代码一定会执行吗?
所有的书上都在说,finally块一定会被执行,但是如果你去面试,面试官很有可能会问你:“finally块一定会被执行么?”这个时候,你就蒙了。事实上finally块在一种情况下不会被执行:JVM被退出。虚拟机都被推出了,什么都不会执行了。
 

package ExceptionEx; public class FinallyEx { / * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub try { System.exit(0); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }finally{ //do some thing; System.out.println("111"); } } } 

如果调用System.exit(0)那么你会发现不会再打印出111这个字符串了。还有请不要在抛出异常后调用这个退出jvm的方法,编译器会报一个:Unreachable code(代码不会被执行)的错误。

(七)异常抛出的限制

1.如何处理这种限制
这个时候我们或许会要用到上文中提到过的包装方法。将已检查异常当作未检查异常偷梁换柱抛出去。所以有的时候我们说:不要去抛出任何未检查异常,这么绝对的说也是不好的。

--------------------- 
作者:QuinnNorris 
来源:CSDN 
原文:https://blog.csdn.net/quinnnorris/article/details/ 
版权声明:本文为博主原创文章,转载请附上博文链接!

小讯
上一篇 2025-01-27 23:11
下一篇 2025-02-26 08:10

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/16175.html