学习目的
- 了解异常与异常机制的概念
- 了解异常的分类
- 掌握异常的处理方法(try-catch、throws)
- 了解自定义异常
- 了解方法覆盖(方法重写)与异常的关系
一、异常
- 概念
异常是java 提供一个体系结构,当程序代码中出现问题时,它会把错误以及错误的详细信息告诉开发者,使得开发的程序能够更健壮。而开发者又可以把错误信息进行处理后告诉用户。 - 实质
java异常都是一个类或者一个对象它从出错地点被抛出,继承于Object类。在异常类发生时,jvm会创建出异常对象,并携带一些提示错误的信息给开发者,开发者可以通过异常类把信息从中取出来。 - 异常出现
- 语法结构/格式异常:编写的代码不按照指定规则(大小写、标识符名称、数值类型、变量类型等),不满足语法要求时出现异常;
- 调用本地类方法异常:在调用本地类的一些方法时,被调用的本地方法本身就自带异常抛出,并不是只有编译错误才会出现异常。而是开发java语言的开发人员希望你可以解决预想到的异常。
public class ExceptionTest {
public static void main(String[] args) throws Exception {
input();
}
public static void input() throws Exception{
new FileInputStream("文件路径");
}
}
//FileInputStream源码的构造方法,自带抛出FileNotFoundException异常
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}
二、异常分类
- 分类
异常主要分为错误error(不可处理)、一般性异常(受控异常)、运行期异常(非受控异常)等。 - 重点
- 所有的错误Error 和 异常Exception都实现 Throwable接口;
- 所有异常都是可抛出的。
-
异常的继承结构体系
异常的层次结构.png
2.1 错误Error
- 概念
Error 是 Throwable的子类,用于指示合理的应用程序不应该试图捕获的严重问题。如果应用程序出现了错误 Error,将导致程序无法恢复,只能重新启动应用程序。 - Error异常
- 最典型的Error异常:OutOfMemoryError<内存溢出>;
2.2 受控异常<编译期异常,Exception类>
- 概念
受控异常指的是可以控制的异常,使用Exception表示,又称为一般性异常。Exception 类及其子类是 Throwable的一种形式,它指出了合理的应用程序想要捕获的条件。 - 出现时期
受控异常 通常出现在编译期,需要开发者必须显示的处理,不显示处理该程序将无法编译通过。
2.3 非受控异常<运行期异常,RuntimeException类>
- 概念
非受控异常指的是 无法控制的异常,通常出现在运行期,不受开发者控制。RuntimeException 是那些可能在 Java虚拟机正常运行期间(JVM内部)抛出的异常的超类。 - 出现时期
非受控异常出现在编译通过后,执行代码运行期间。非受控异常发生在在运行期间,不能由程序员处理。
三.异常声明
3.1 受控异常声明
- 概念
受控异常指的是可以受程序员控制的异常,一般在方法及方法体中进行声明的异常,称为受控异常。 - 要求
必须对声明的每一个异常进行调用处理,尽可能使用 try...catch每一个声明的异常,避免编译出错。
3.2 非受控异常声明
- 概念
不能由程序员自主控制,需要交由jvm虚拟机进行处理的异常称为非受控异常。若声明的异常为非受控异常,直接在方法声明处进行抛出给JVM虚拟机进行捕获处理。不需要使用 try...catch,而是在方法声明后面throws抛出。
3.3 异常的捕获顺序
- 定义
异常的捕获顺序能够影响程序的编译,也可以决定程序的执行效率,对异常的捕获若是详细到每一个子异常且有序,有利于后期测试。 - 遵循规律
异常的捕获顺序:从小到大。
四.异常处理(try、catch 、 finally)
- 概念
异常处理指的是需要对可能发生的异常进行问题处理,保证程序的可行性和代码的健壮性。 - 处理方式
- throws XXXException:异常抛出。在异常发生时,通常将该异常抛给调用者或上级处理,把"黑锅丢给别人"。该种方式可以一直不断往上抛(最终上级为jvm),直到抛给main方法也不进行处理时,jvm终止程序。
- try...catch:异常捕获。即异常发生时,发生异常的本体立即进行异常捕获处理,以免异常往上抛。主动担当,快速处理。
4.1 try-catch异常处理
- 处理机制
try-catch异常处理机制是通过在try语句块中执行可能出现异常的程序;
在catch语句中捕获出现的具体异常对象,并使用该异常对象去处理异常;catch捕获的异常可以是出现本次的具体异常对象,也可以是具体异常的父类对象。 - 语法结构
try {
//可能出现的异常java语句
}catch(OneException e1) {
//捕获异常1,通过异常类对象e1获取详细的异常信息
//e1是栈区的引用,指向在堆内存中的OneException异常对象
}catch(TwoException e2) {
//捕获异常2,通过异常类对象e2获取详细的异常信息
//e2是栈区的引用,指向堆内存中的TwoException
}finally {
// 最终都会执行的java语句
}
- 执行流程
- try语句:try 语句中书写 可能产生异常的代码;
- catch语句:try 后面跟着catch语句,catch语句可以有一个或多个,类似else if语句;
- catch 语句中书写 需要处理的捕获异常对象;
当 try 语句中的代码出现异常时,编写在异常语句后的java语句不会再执行,而是马上会跳转到 相应异常的catch语句块中进行异常捕获和处理。
catch捕获的异常从小到大,并尽可能对应捕获到try中出现的每一种异常类型。 - 如果try中无异常发生,则不会跳转到 catch中 。
- 注意点
- catch捕获的异常对象e1:异常对象不是在进行异常处理的本类中new出来,而是在异常发生时,异常对象本类的底层源码中调用其构造方法 throw new出。e1只是一个"形参"从异常抛出类传到异常捕获类中。
- 捕获父类异常:不建议使用Exception父类捕获全部异常,会不利于查找出try语句中哪个语句发生什么类型的异常,不利于调试。
4.2 try-catch-finally异常处理
- 处理机制
try-catch-finaly异常处理机制是通过在try语句块中执行可能出现异常的程序;
在catch语句中捕获出现的具体异常对象,并使用该异常对象去处理异常;
在finally语句中,不管catch语句是否捕获并处理异常,finally语句都会执行。 - 语法结构
try {
//finally必须结合try语句才能存在
}catch(OneException e1) {
//catch语句有无都行
}finally {
//finally在任何情况下都会执行,通常在 finally里关闭资源
}
执行流程
注意点
- finally先于jvm:finally语句只有在jvm退出前执行, java 虚拟机已经退出,则不会执行finally语句(在finally前面的语句中执行System.exit(-1)方法)。
- return与finally执行关系:return若在fianlly语句之前执行,则结束整个方法,finally不再执行;若return在finally执行后再执行,return返回的数据可能是finally中最终返回的数据。
public static void main(String[] args) {
int i1 = 100;
int i2 = 10;
try {
int i3 = i1/i2;
System.out.println(i3);
//return;//结束整个方法
System.exit(-1); //exit(-1)退出虚拟机,finally语句不再执行
}catch(ArithmeticException ae) {
ae.printStackTrace();//打印异常在栈内存结构中的信息
}finally {
//只有 java 虚拟机退出不会执行 finally
System.out.println("----------finally---------");
}
}
4.3 throws异常抛出
- 处理机制
除了使用try-catch在方法体中手动的捕获异常、处理异常外,java还提供一种使用throws在方法声明上抛出异常进行处理异常,将异常不断往上、往调用方抛出,最终抛到虚拟机,让虚拟机进行处理。 - 语法结构
//异常产生的地点
class A{
public void add(){
throw new xxxException();
}
}
- 执行流程
- 异常由实际产生的具体地点抛出;
- 不断抛给调用方,调用方因调用 异常产生地的实例方法或属性,将异常抛给上级;
- 不断循环,循序渐进,最终抛到main方法中,main方法将异常抛给最后的jvm虚拟机,又虚拟机进行处理;
- 若虚拟机都无法进行处理,程序终止。
- 注意点
在JDK8中,catch()语句捕获的异常类型除了可以使用具体异常、父类异常等,还可以使用 "异或"运算符 表示选择性捕获异常。
try {
//创建输入流
FileInputStream fis = new FileInputStream("D:\\curse\\JavaSE\\document\\JavaSE-面向对象.pdf");
} catch(FileNotFoundException | ArithmeticException | NullPointerException e) { // 异或 | 运算符选择性捕获异常
System.out.println("文件不存在?数学异常?空指针异常?都有可能!");
}
4.4 异常处理方法
- getMessage()方法
getMessage()方法用在catch语句中,主要用于获取所捕获异常对象的描述信息,所捕获异常对象的描述信息通常由该异常类中(底层的出错点)throw抛出。
public static void main(String[] args) {
try {
m1();
} catch (FileNotFoundException e) {
String msg = e.getMessage(); // 获取异常的简单描述信息
System.out.println(msg); //C:\\jetns-agent.jar
}
}
private static void m1() throws FileNotFoundException {
new FileInputStream("C:\\jetns-agent.jar");//参数就是异常信息
}
- printStackTrace()方法
printStackTrace()方法是打印所捕获异常的堆栈信息,适用于程序调试阶段。通过打印栈结构,从而追踪错误信息。
jvm多线程采用异步方式调用printStackTrace()方法,与调用main方法的不是同一个线程。
public static void main(String[] args) {
try {
m1();
} catch (FileNotFoundException e) {
//打印异常堆栈追踪信息,实际开发中建议使用
e.printStackTrace();//打印出异常问题具体错在哪(一行)
}
//往控制台执行以下打印输出程序 不耽误执行,服务器不会因为遇到上方异常而宕机
System.out.println("Hello World!");
}
private static void m1() throws FileNotFoundException {
m2();
}
private static void m2() throws FileNotFoundException {
m3();
}
private static void m3() throws FileNotFoundException {
new FileInputStream("C:\\jetns-agent.jar");
}
4.5 异常查看方式
- 查看堆栈信息
通过异常对象调用printStackTrace()方法,在控制台打印出栈结构,就会追踪到所有错误信息(具体出现在哪一行)。 - 查看错误信息重点
查看错误的重点应在于自己编写的项目路径下的错误位置,不需要关注java本地路径出现的错误(java源码不会出错)。
一般打印的栈结构信息,前半部都是java本地路径信息,后半部才是自己编写项目出现错误的位置及原因,且自己编写的异常信息是自上而下逐步影响的,因此应从自己编写出现的第一个异常开始查看。
//如以下在控制台打印的栈结构错误信息
// java本地路径出现的异常
java.io.FileNotFoundException: C:\jetns-agent.jar (系统找不到指定的文件)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.(FileInputStream.java:155)
at java.base/java.io.FileInputStream.(FileInputStream.java:110)
// 自己编写的代码出现异常的位置,重点在自己编写出错的位置
at ExceptionTest09.m3(ExceptionTest09.java:31)
at ExceptionTest09.m2(ExceptionTest09.java:27)
at ExceptionTest09.m1(ExceptionTest09.java:23)
at ExceptionTest09.main(ExceptionTest09.java:14)
因为31行出问题导致了27行
27行出问题导致23行
23行出问题导致14行。
应该先查看31行的代码。31行是代码错误的根源。
五、自定义异常
- 概念
虽然jdk源码中提供了许多种类的异常给予程序员使用,但是现实中依然存在许多未被收录的异常,因此需要捕获一些除java本身提供的以外的异常来满足开发需求和提高效率。 - 遵循规律
自定义异常通常继承 Exception 或 RuntimeException类,而到底继承哪个,看具体情况来定。
- 自定义受控异常:继承Exception类,且方法体内 throw抛出时必须进行声明,被catch捕获后必须处理。
- 自定义非受控异常:继承RuntimeException类,可以不用在方法体内 throw抛出,但必须方法声明后面throws 抛出给JVM处理。
- 自定义异常要求
- 继承父类:异常是一个类或对象,必须继承Exception 或RuntimeException类,提供构造方法,并且显示调用父类的构造方法(super);
- 显式调用Throwable接口的无参构造方法;
- 重写Throwable接口的带参构造方法,参数为异常错误提示信息。
- 代码示例
//自定义受控异常(编译期异常)
public class MyException extends Exception{
//java源码中每一个异常类的定义都实现(继承)了Throwable接口的构造方法
public MyException() {
//显示调用父类Throwable接口的构造方法
super();
}
public MyException(String message) {
//显示调用父类Throwable接口的带参构造方法
super(message);
}
}
//测试受控异常
public class MyExceptionTest {
public static void main(String[] args) {
//创建自定义受控异常对象
MyException me = new MyException();
//自定义异常为编译时异常,在main方法中不能抛给jvm处理
try{
//method1(10,0);
method1(13,5);
}catch (MyException e){
//编译期异常,必须手动捕获解决
System.out.println(e.getMessage());
}
}
//自定义非受控异常(运行时异常)
public class MyRunTimeException extends RuntimeException{
//编译期异常、运行期异常都实现Throwable接口的构造方法
public MyRunTimeException() {
//显示调用父类接口的构造方法
super();
}
public MyRunTimeException(String message) {
super(message);
}
}
//测试自定义非受控异常
public class MyRunTimeExceptionTest {
public static void main(String[] args) throws MyRunTimeException{
//创建自定义非受控异常对象
MyRunTimeException mrt = new MyRunTimeException();
/*未进行throws处理时
method2(0,23);//这里出现异常,不能继续往下执行
method2(13,26);
*/
method2(13,26);
}
private static void method2(int i,int j) throws MyRunTimeException{
if (i == 0){
throw new MyRunTimeException("因数为0,不能进行相乘!");
}
int k = i*j;
System.out.println(k);
}
}
5.1 自定义异常的实际运用
- 用处
当编写的代码程序可能出现的异常,已经超出java源码提供的异常本身,就可以使用自定义异常来完成异常处理。 - 应用
- 定义异常:无参构造中,手动调用Throwable接口的构造方法;带参构造中手动调用Throwable接口的带参构造方法;
- throw异常:使用throw new 异常对象(),将异常信息抛出。
5.2 方法覆盖与异常
- 子类重写的方法不能抛出比父类方法更多的异常,也不可以抛出比父类更大的异常,但可以抛出父类方法异常的子异常;
- 异常是类,即支持继承<接口继承 和 类继承>,因此产生父类异常和子类异常。
常见面试题
- final、finalize 和 finally的关系?
答:
- final是关键字,表示最终的。由final修饰的变量不可修改也不可重新赋值;由fianl修饰的类为最终的,不可继承。
- finalize是JVM的一个方法,调用该方法可以释放资源。
- finally也是关键字,是一个语句,常与try语句结合使用,且finally语句无论何时都会执行
System.exit(0);> - finally语句的无论如何都执行 是相对于同程序块的try语句已经执行。若try语句未执行前就已经出错或者已经return,程序就结束了,也就谈不上finally语句一定会执行了。
- throws 和 throw 的区别?
答:
- thorw 是声明异常,后面一般是throw new Exception()创建出异常对象。而该对象就是catch语句中捕获得到的异常对象,只不过是在产生异常对象的位置,将其内存地址传递过来,到处理异常的类中。即该异常对象是跨越了两个类之间的一个引用形参。
- throws是抛出异常,一般是在方法后面声明,声明该方法可能会产生的异常。声明后的异常,会抛给调给方法的对象。
- throw相当于 return 语句,throw执行,下面的代码不再执行。
区别于return语句,throw 虽然类似 return 语句,但 finally 依然会执行。而return执行,整个方法结束,finally也不会再执行