1、异常概述与异常体系结构
java.lang.Throwable
java.lang.Error
:一般不编写针对性的代码进行处理。java.lang.Exception
:可以进行异常的处理。
- 编译时异常(checked),举例如下:
IOException
:FileNotFoundException
ClassNotFoundException
- 运行时异常(unchecked,RuntimeException),举例如下:
NullPointerException
ArrayIndexOutOfBoundsException
ClassCastException
NumberFormatException
2、异常对象的抓抛模型
过程一:”抓”:
可以理解为异常的处理方式:
1、try-catch-finally
2、throws
过程二:”抛”:
程序在正常执行的过程中,一旦出现异常,就会在异常代码处生成一个对应异常类的对象,并将此对象抛出。一旦抛出对象以后,其后的代码就不再执行。
异常对象的产生方式:
1、系统自动生成的异常对象。
2、手动的生成一个异常对象,并抛出(throw
)。异常处理方式1:try-catch-finally
使用示例: ```java try{
//可能出现异常的代码
}catch(异常类型1 变量名1){
//处理异常的方式1
}catch(异常类型2 变量名2){
//处理异常的方式2
}catch(异常类型3 变量名3){
//处理异常的方式3
}
….
finally{
//一定会执行的代码
}
1、使用`try`将可能出现异常代码包装起来,在执行过程中,一旦出现异常,就会生成一个对应异常类的对象,根据此对象的类型,去`catch`中进行匹配。 一旦`try`中的异常对象匹配到某一个`catch`时,就进入`catch`中进行异常的处理,一旦处理完成,就跳出当前的`try-catch`结构(在没有写`finally`的情况),继续执行其后的代码。<br />2、`catch`中的异常类型如果没有子父类关系,则谁声明在上,谁声明在下无所谓;`catch`中的异常类型如果满足子父类关系,则要求子类一定声明在父类的上面,否则会报错。<br />3、常用的异常对象处理的方式:
- `getMessage()`
- `printStackTrace()`
4、在`try`结构中声明的变量,出了该结构以后,就不能再被调用。<br />5、`finally`是可选的。<br />6、`finally`中声明的是一定会被执行的代码,即使`catch`中又出现异常了,`try`中有`return`语句,`catch`中有`return`语句等情况。<br />像数据库连接、输入输出流、网络编程Socket等资源,JVM是不能自动的回收的,我们需要自己手动的进行资源的释放。此时的资源释放,就需要声明在finally中。<br />6、`try-catch-finally`结构可以嵌套。<br />7、使用体会
- 体会1:使用`try-catch-finally`处理编译时异常,是得程序在编译时就不再报错,但是运行时仍可能报错。相当于我们使用`try-catch-finally`将一个编译时可能出现的异常,延迟到运行时出现。
- 体会2:开发中,由于运行时异常比较常见,所以我们通常就不针对运行时异常编写try-catch-finally了,针对于编译时异常,我们说一定要考虑异常的处理。
<a name="rUufd"></a>
### 异常处理方式2:throws + 异常类型
1、"throws + 异常类型"写在方法的声明处,指明此方法执行时,可能会抛出的异常类型。一旦当方法体执行时,出现异常,仍会在异常代码处生成一个异常类的对象,此对象满足throws后异常类型时,就会被抛出。异常代码后续的代码,就不再执行!<br />2、体会:
- `try-catch-finally`:真正的将异常给处理掉了。
- `throws`的方式只是将异常抛给了方法的调用者,并没有真正将异常处理掉。
3、开发中如何选择使用`try-catch-finally`还是使用`throws`?
- 如果父类中被重写的方法没有`throws`方式处理异常,则子类重写的方法也不能使用`throws`,意味着如果子类重写的方法中有异常,必须使用`try-catch-finally`方式处理。
- 执行的方法a中,先后又调用了另外的几个方法,这几个方法是递进关系执行的。我们建议这几个方法使用`throws`的方式进行处理,而执行的方法a可以考虑使用`try-catch-finally`方式进行处理。
<a name="i7Twl"></a>
### 手动抛出异常:throw
![image.png](https://cdn.nlark.com/yuque/0/2021/png/2853856/1631758819172-79661a72-8d2a-475b-b30a-b4083a5bad18.png#clientId=u47becd82-6fda-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=177&id=ueb605a9a&margin=%5Bobject%20Object%5D&name=image.png&originHeight=662&originWidth=1762&originalType=binary&ratio=1&rotation=0&showTitle=false&size=533575&status=done&style=none&taskId=u996e4d93-3d5a-47c2-bf21-6bc18e2772c&title=&width=472)
<a name="eHuH3"></a>
### 用户自定义异常类
![image.png](https://cdn.nlark.com/yuque/0/2021/png/2853856/1631758848701-e50f2bbd-e471-4e9d-ac57-f484fc551f7c.png#clientId=u47becd82-6fda-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=177&id=u9f11eda6&margin=%5Bobject%20Object%5D&name=image.png&originHeight=644&originWidth=1710&originalType=binary&ratio=1&rotation=0&showTitle=false&size=482072&status=done&style=none&taskId=ud6124e59-905a-4285-99c5-b1693e3e595&title=&width=469)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2853856/1631758865149-61828e91-210b-4d40-9d6e-71a0f6eb0dd5.png#clientId=u47becd82-6fda-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=228&id=u39230471&margin=%5Bobject%20Object%5D&name=image.png&originHeight=850&originWidth=1610&originalType=binary&ratio=1&rotation=0&showTitle=false&size=529568&status=done&style=none&taskId=u853c505f-8a7d-4e2c-bbd2-925dceffac1&title=&width=431)
<a name="lUcqi"></a>
## 3、总结
![image.png](https://cdn.nlark.com/yuque/0/2021/png/2853856/1631758894092-384758e7-dbe9-41cb-a27d-baa65d4d776b.png#clientId=u47becd82-6fda-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=185&id=ufd4bd0ab&margin=%5Bobject%20Object%5D&name=image.png&originHeight=932&originWidth=1824&originalType=binary&ratio=1&rotation=0&showTitle=false&size=694604&status=done&style=none&taskId=u686a0cdb-a672-426f-8329-65adead7a6e&title=&width=362)
<a name="epv1H"></a>
## 4、常见面试题
<a name="Y1MSw"></a>
### 1. Error 和 Exception 区别是什么?
Error 类型的错误通常为虚拟机相关错误,如系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,JAVA 应用程序也不应对这类错误进行捕获,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复;<br />Exception 类的错误是可以在应用程序中进行捕获并处理的,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。
<a name="YYBMZ"></a>
### 2. 运行时异常和一般异常(受检异常)区别是什么?
运行时异常包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。 Java 编译器不会检查运行时异常。<br />受检异常是Exception 中除 RuntimeException 及其子类之外的异常。 Java 编译器会检查受检异常。<br />RuntimeException异常和受检异常之间的区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常(RuntimeException)。一般来讲,如果没有特殊的要求,我们建议使用RuntimeException异常。
<a name="J8n24"></a>
### 3. JVM 是如何处理异常的?
在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。
<a name="y5XP6"></a>
### 4. throw 和 throws 的区别是什么?
Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。
throws 关键字和 throw 关键字在使用上的几点区别如下:<br />throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。<br />throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。
<a name="fFZx4"></a>
### 5. final、finally、finalize 有什么区别?
final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。<br />finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码放入finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。<br />finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,Java 中允许使用 finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
> finalize是当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。子类重写 finalize 方法,以配置系统资源或执行其他清除。
> Java 编程语言不保证哪个线程将调用某个给定对象的 finalize 方法。但可以保证在调用 finalize 时,调用 finalize 的线程将不会持有任何用户可见的同步锁定。如果 finalize 方法抛出未捕获的异常,那么该异常将被忽略,并且该对象的终结操作将终止。
> 在启用某个对象的 finalize 方法后,将不会执行进一步操作,直到 Java 虚拟机再次确定尚未终止的任何线程无法再通过任何方法访问此对象,其中包括由准备终止的其他对象或类执行的可能操作,在执行该操作时,对象可能被丢弃。
> 使用finalize还需要注意一个事,调用super.finalize(); 一个对象的finalize()方法只会被调用一次,而且finalize()被调用不意味着gc会立即回收该对象,所以有可能调用finalize()后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会调用finalize(),产生问题。 所以,推荐不要使用finalize()方法,它跟析构函数不一样。
<a name="FH1of"></a>
### 6.NoClassDefFoundError 和 ClassNotFoundException 区别?
NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。<br />引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致;
ClassNotFoundException 是一个受查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。
<a name="mZiXl"></a>
### 7. try-catch-finally 中哪个部分可以省略?
答:catch 可以省略,finally也可以省略。<br />在受检异常里try catch finally 中 finally是可以省略的,运行时异常中,catch finally必须有一个。实践得出来的<br />原因<br />更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运行时异常+普通异常。也就是说,如果你只用try去处理普通异常却不加以catch处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以catch可以省略,你加上catch编译器也觉得无可厚非。
理论上,编译器看任何代码都不顺眼,都觉得可能有潜在的问题,所以你即使对所有代码加上try,代码在运行期时也只不过是在正常运行的基础上加一层皮。但是你一旦对一段代码加上try,就等于显示地承诺编译器,对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异常,编译器要求必须用catch捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally扫尾处理,或者加上catch捕获以便进一步处理。<br />至于加上finally,则是在不管有没捕获异常,都要进行的“扫尾”处理。
<a name="yC7mO"></a>
### 8. try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?
答:会执行,在 return 前执行。<br />注意:在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java 中也可以通过提升编译器的语法检查级别来产生警告或错误。<br />代码示例1:
```java
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
/*
* return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了
* 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40
* 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30
*/
} finally {
a = 40;
}
return a;
}
执行结果:30
代码示例2:
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
} finally {
a = 40;
//如果这样,就又重新形成了一条返回路径,由于只能通过1个return返回,所以这里直接返回40
return a;
}
}
执行结果:40
9. 类 ExampleA 继承 Exception,类 ExampleB 继承ExampleA。
有如下代码片断:
try {
throw new ExampleB("b")
} catch(ExampleA e){
System.out.println("ExampleA");
} catch(Exception e){
System.out.println("Exception");
}
请问执行此段代码的输出是什么?
输出:ExampleA。(根据里氏代换原则[能使用父类型的地方一定能使用子类型],抓取 ExampleA 类型异常的 catch 块能够抓住 try 块中抛出的 ExampleB 类型的异常)
面试题 - 说出下面代码的运行结果:
class Annoyance extends Exception {
}
class Sneeze extends Annoyance {
}
class Human {
public static void main(String[] args)
throws Exception {
try {
try {
throw new Sneeze();
} catch ( Annoyance a ) {
System.out.println("Caught Annoyance");
throw a;
}
} catch ( Sneeze s ) {
System.out.println("Caught Sneeze");
return ;
} finally {
System.out.println("Hello World!");
}
}
}
结果
Caught Annoyance
Caught Sneeze
Hello World!
10. 常见的 RuntimeException 有哪些?
ClassCastException(类转换异常)
IndexOutOfBoundsException(数组越界)
NullPointerException(空指针)
ArrayStoreException(数据存储异常,操作数组时类型不一致)
还有IO操作的BufferOverflowException异常
11. Java常见异常有哪些?
java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。
java.lang.InstantiationError:实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常.
java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。
java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。
java.lang.ClassCastException:类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。
java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
java.lang.ArithmeticException:算术条件异常。譬如:整数除零等。
java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。
java.lang.InstantiationException:实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。
java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。
java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异常。
java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。
java.lang.NumberFormatException:数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。
java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。