异常
Java异常概述
异常,异常,即是程序运行出现了不正常的情况,报错,并导致程序终止
- Java当中一切皆对象,当程序产生异常,JVM会把异常信息封装成一个对象(类)
- 类中封装着问题的名称,产生的原因、描述等多个属性信息存在
- 以及对这些信息进行操作的一系列方法(属性+行为)
- 这些类通过继承层次构成了Java的异常体系
- 学习异常之前,需要明确的一个重要点
- 异常的类和对象中只是存了异常的信息
- 包括异常的原因,异常的种类等等
- 但是何时抛出异常,怎么处理异常,不是由异常对象决定的
Java程序运行时碰到了一系列的问题
- 编译时期,必须要检查处理的异常,不检查不能通过编译。即便发现了问题,仍然可以解决
- 运行期间,无法预料的问题,但是出现问题后,我们仍然能解决它
- 运行期间,无法预料的问题,但是出现问题后,无法解决它,是一个严重的错误
我们一般根据问题能不能处理,也就是问题的严重程度,来区分异常和错误
- Java程序运行时碰到了一系列的问题
- 严重问题
- 运行时,无法预料,且无法解决的错误
- 一般问题
- 编译时要检查处理的异常,出现问题,可以被解决
- 运行时,无法预料,且能够被解决的问题
- 严重问题
Java异常体系确实就是这样划分的
-
Throwable(祖先类)
-
Error
-
Exception
-
RuntimeException 运行时异常
RuntimeException和它的所有子类都是运行时异常
-
非RuntimeException 编译时异常
-
-
-
Throwable是Java一切错误和异常的父类,是继承层次中的祖先类
- 表示可以由程序显式或者JVM抛出的问题
-
Error是严重问题,无法被解决
- Error描述了Java运行时虚拟机内部错误和资源耗尽错误
- 对于Error,程序自己是无能为力的,仅靠程序本身是无法恢复和和预防
- 于是程序只能尽量安全得保存数据, 然后终止程序,并通知用户去解决
- 常见的Error是栈溢出,或者堆溢出这些错误
-
Exception是一般问题,能够被解决
- RuntimeException,指的是在程序运行期间,发生的一般问题,称之为运行时异常
- 这种问题无法在编译时检查和预料,只有到程序运行后才能显现问题
- 例如用null调用方法,数组使用错误的下标,错误的强制类型转换
- RuntimeException可以写代码进行正常的处理,属于一般问题
需要明确的是:
运行时异常绝大多数都是因为编码问题所导致的
也就是可以避免的异常,当程序抛出该异常后,最好能够重构代码,修正问题
- 非RuntimeException,指的是在编译时期,就需要显式的检查并处理的异常,称之为编译时异常
- 部分书籍也称其为,受检查的异常(Checked Exception)
- 这种异常必须在编译期检查和处理
- 例如打开一个文件夹(要考虑该文件夹存不存在)
- 克隆一个对象(要检查该类是否实现Cloneable接口)
- Exception的子类中,只要不是RuntimeException的子类,那必然是非RuntimeException
- RuntimeException,指的是在程序运行期间,发生的一般问题,称之为运行时异常
Error和Exception的异同
- 两者都继承自Throwable类,共同构成了Java的异常体系
- Error类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等
- 对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防
- 遇到这样的错误,只好尽量安全得保存数据, 然后终止程序,并通知用户去解决
- Error一般都不由程序显式的抛出,而是由JVM抛出
- Exception类表示程序可以处理的异常,可以捕获且可能恢复
- 遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意地终止程序
- Exception又分为RuntimeException(运行时异常)和非RuntimeException(编译时异常)
- 运行时异常不受编译器检查,在程序运行期间发生,一旦发生,若不处理就会导致程序终止
- 运行时异常包括所有RuntimeException的子类
- 编译时异常受编译器检查,必须显式地处理该异常,才能够通过编译
- 编译时异常包括所有Exception的子类中,非RuntimeException的子类的类
- 运行时异常不受编译器检查,在程序运行期间发生,一旦发生,若不处理就会导致程序终止
补充编译时异常和运行时异常区分
运行时异常:如NullPointerException、IndexOutOfBoundsException。这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,可以从逻辑角度出发去处理,尽可能避免这类异常的发生。
——————————————————————————————————————
编译时异常:这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。
异常的处理
Java程序的异常处理分为两种方式
- 自己处理
- 向上抛出
-
某个方法发生了异常
- 自己解决
- 抛给该方法的调用者
-
调用者(方法)拿到了异常
- 自己解决
- 抛给该方法的调用者
-
这个过程可以将异常一直向方法的调用者抛出,但不可能是无限的
…
-
最终main方法,程序的入口方法拿到了该异常,它可以选择
- 自己解决
- 继续抛出
-
这个时候,能接收main方法抛出的异常的,就只有JVM
- JVM必须自己处理该异常
Java默认的异常处理机制
如果我们在程序中,不写任何和异常处理相关的代码,Java程序仍然能够对异常进行处理
- 如果错误产生在main方法中
- 当我们的代码执行到错误行数之前,代码是正常执行的
- 当我们的代码执行到错误行数时,JVM会终止程序的执行,抛出一个该异常信息封装成的对象
- 将该对象中的异常信息,打印到控制台上,告诉程序员发生了什么问题
- 发生错误之后的语句,都不执行了
- 如果错误产生在main方法当中的另一个方法中
- 当程序执行到该方法的错误行数时,JVM会终止程序的执行
- 向上给方法的调用者抛出一个该异常信息封装成的对象
- 一直向上抛出,直到抛给main方法,main方法最终抛给JVM
- 发生异常之前的语句正常执行,但是之后的语句都不执行了
- 当程序执行到该方法的错误行数时,JVM会终止程序的执行
注意事项:
这种默认处理机制只是针对运行时异常的,对于编译时异常,程序员必须在编译时手动处理它
自定义(手动)异常处理
显然,Java默认的异常处理机制,总会导致程序终止执行,这不能够满足我们的需求
我们需要手动显式的来处理异常,以达到自己的目的
捕获异常,自己处理
Java提供了结构try…catch用来捕获并处理异常
单分支try…catch
-
顾名思义,其语法为
-
try { //可能出现异常的,正常的代码逻辑 } catch(要捕捉的异常对象) { //每一个catch分支对应一个异常处理器 //在catch分支中处理具体类型的代码异常 }
-
-
如果try代码块中发生了异常,那么JVM就会收集这个异常的信息,封装成对象
-
catch语句中需要填入一个对象引用作为匹配,而不是使用类型来匹配
- 这个引用接收(指向)JVM封装的对象
-
try和catch代码块都是代码块,它们里面定义的变量都是局部变量
-
catch语句中可以填入多个对象来作为匹配
-
catch(要捕捉的异常类型1 | 要捕捉的异常对类型2 | 要捕捉的异常类型3 对象名...){ }
-
注意:无论你能够匹配多少种异常类型,始终都只有一个异常对象被接收,对象名只写一个
-
-
catch当中填入要捕捉的异常类型,如果能够匹配这个对象,那么就会执行catch代码块中的代码
- 怎么算匹配?
- 可以就是这个类的对象
- 也可以是这个类的子类对象
- catch代码块中语句可以对该异常进行处理
-
一旦匹配成功,catch中写的异常对象就会接收JVM抛出的异常对象
-
匹配失败,那么程序依然会自动向上抛出异常,直到JVM默认处理
-
try代码块中发生了异常,在catch代码块中被处理了,那么程序仍然能够继续执行
-
单分支try…catch使用注意事项
-
try…catch会显著的影响代码结构,严重影响代码可读性
- 所以应该把尽量少的代码放入try中,最好是产生异常的那一行代码
-
如果catch不能匹配异常对象,那么不会执行catch代码块中的内容
-
只有当try代码块中产生了异常,catch才有机会执行,没有异常不执行,不匹配也不执行
-
try代码块中某个位置产生了异常,那么try中的异常后面的代码就不继续执行了
- 也就是说try当中要么不产生异常,要么只会产生一个异常
小练习
- 看代码说执行结果
try {
System.out.println("before");
System.out.println(10/0);
System.out.println("after");
}
catch (ArithmeticException e){
System.out.println("发生了异常");
}
System.out.println("try..catch之后");
- 看代码说执行结果
try {
System.out.println("before"); // 执行
System.out.println(10/0); // 执行,异常,到catch
System.out.println("after"); // 不执行
}
catch (RuntimeException e){
System.out.println("发生了异常"); //执行
}
System.out.println("try..catch之后"); // 执行
- 看代码说执行结果
try {
System.out.println("before");// 执行
System.out.println(10/0); // 执行,产生异常
System.out.println("after");
}
catch (NullPointerException e){
System.out.println("发生了异常"); // catch异常不匹配,catch不执行
}
System.out.println("try..catch之后"); // 不执行
- 看代码说执行结果
try {
System.out.println("before"); // 执行
System.out.println(10/10); // 执行
System.out.println("after"); //执行
}
catch (NullPointerException e){
System.out.println("发生了异常"); //不执行
}
System.out.println("try..catch之后"); //执行
获取捕获异常对象中的异常信息
既然能捕获该异常对象,那么获取其中的异常信息也是势在必行的
使用捕获的异常对象,能够调用的常用API有(异常对象进行调用,比如catch后括号中定义的对象调用)
//获取异常信息,返回字符串。打印的实际上是异常产生的原因
(不使用)String getMessage()
//获取异常类名和异常信息,返回字符串。 打印的是异常产生的原因和所在类
(不使用)String toString()
//获取异常类名和异常信息,以及异常出现在程序中的位置
(推荐使用)void printStackTrace()
//使用IO流,将异常内容保存在日志文件中,以便查阅,早已过时,了解即可
(过时的日志处理方式)printStackTrace(PrintStream s)
// 给个例子,但是具体有啥用,有待我研究
ArithmeticException ae = new ArithmeticException("心情不好,想抛出异常!");
ae.printStackTrace();
多分支try…catch
如果代码只会发生一个异常,那么单分支的try…catch就够用了,那么如果发生多个异常呢?
-
语法为
-
try { //可能出现异常的,正常的代码逻辑 } catch(要捕捉的异常对象1) { //每一个catch分支对应一个异常处理器 //在catch分支中处理具体类型的代码异常 }catch(要捕捉的异常对象2) { //每一个catch分支对应一个异常处理器 //在catch分支中处理具体类型的代码异常 } ...
-
-
该格式和单分支try…catch并无实质不同,只是多个几个catch分支而已
-
多分支try…catch的匹配流程
- 根据实际的异常对象的类型,和catch中声明的异常类型,从上到下一次做类型匹配
- 一旦通过类型匹配,发现实际异常对象的类型和catch中的异常对象类型匹配
- 就把该异常对象交给这个catch分支进行处理(异常处理器)
- 没有相匹配catch代码块的异常,那么程序依然会自动向上抛出异常,直到JVM默认处理
多分支try…catch使用注意事项
-
多分支的异常处理的执行,有点类似于多分支if-else的执行,一次匹配,只会执行多个catch分支中的一个
-
如果多个catch中处理的是毫无关系的异常,那么catch的顺序并不需要特别注意
-
如果多个catch中处理的异常有父子关系,那么就必须要注意了
- 如果父类异常写在了上面,那么子类异常的catch分支就永远没有机会执行了,并且会报错
-
所以,应该把具体子类放在catch分支的上面作类型匹配,父类放在后面作兜底
-
两种运行时异常
-
NullPointerException: 运行时异常,空指针异常,用一个等于null的引用调用了方法和成员变量 IndexOutOfBoundsException:运行时异常,数组下标越界异常。调用了超过数组最大下标的位置
-
牛刀小试
- 需求:单独处理除0异常,但是空指针和下标越界异常一起处理
try {
//System.out.println(10 / 0);
int[] arr = null;
System.out.println(arr.length);
} catch (ArithmeticException ae) {
ae.printStackTrace();
}
catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
//'catch' branch identical to 'ArithmeticException' branch
System.out.println("同时处理空指针和数组下标越界");
e.printStackTrace();
}
抛出异常,上层处理
程序总会有在异常发生处不想处理该异常的情况,Java提供了向上层抛出异常的解决方案
throws关键字
throws关键字表示在方法上抛出异常
- 在方法声明时使用,声明该方法可能抛出的异常类型
语法:
方法名(形参列表) throws 异常列表{
}
注意事项:
-
运行时异常会自动向上抛出,不用我们手动throws
- 我们只需要手动throws编译时异常
-
如果方法抛出一个编译时异常,可以在语法层面,强制要求方法调用者处理该异常
-
异常列表可以是多个异常类,但是注意用逗号隔开
-
列表中出现的异常如果有父子关系,那么编译器只会强制要求处理父类
- 所以尽量抛出同级别的异常
private static void testThrowsMethod1() throws ParseException, CloneNotSupportedException {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
Date date1 = null;
date1 = simpleDateFormat.parse("2020/10/17 9:55:02");
System.out.println(date1.getTime());
Demo d = new Demo();
d.clone();
}
throws的作用:在方法声明中提前声明可能抛出的异常,如果该异常是编译时异常,那么方法的调用处就必须
显式处理该异常。也就说可以提前警告方法调用者,让它做好应对异常的准备
方法覆盖中的异常列表匹配问题
首先,异常说明属于方法声明的一部分,紧跟在形式参数列表之后
方法的声明中加了throws关键字表示所有要抛出的潜在异常类型后
方法在重写的时候也会发生一些变化
- 总体上的原则是:子类中的覆盖方法,不能比父类中的方法抛出更多异常(单指编译时异常)
- 因为Java中存在多态现象,当用父类引用调用方法时, 如果允许子类重写后有更多的异常,那么就没有办法处理
- 运行时异常显式抛出对程序并无影响,那么子类重写方法可以多抛出运行时异常
- 如果子父类方法,完全抛出相同的异常,允许进行方法的重写
- 如果父类方法没有抛出异常,子类重写方法,要么也不抛出异常
- 要么就只能抛出运行时异常(本身就是自动的,不算多)
- 如果父类方法抛了异常,那么
- 子类重写方法可以选择完全不抛出异常
- 如果父类方法抛出的是RuntimeException,那么子类重写方法也只能抛出RuntimeException
- 种类不限制,允许类型不同
- 父类方法抛出一个RuntimeException子类,子类方法重写可以是RuntimeException
- 如果父类方法抛出的是编译时异常,那么子类重写方法
- 可以抛出相同的编译时异常,但不能抛出不同的编译时异常
- 抛出所有运行时异常
- 不可以抛出Exception
- 如果父类方法直接抛出Exception
- 那么子类重写方法就可以抛出任何异常了
我们其实并不需要特别记忆这些规则,实际开发中,我们并不是像老师上课一样,需要一点不能出错。我们可以不断的尝试,然后最终提交出一份正确的代码。这样,我们仍然是一名优秀的Java开发工程师。如果面试中被问到,建议直接说子类中的覆盖方法,不能比父类中的方法抛出更多异常即可。
- 建议在开发中,子父类重写方法拥有一致的抛出异常列表
- 避免自找麻烦
throw
在很多时候,我们不满于在方法声明中声明要抛出的异常的类型,我们想要直接在方法内部抛出异常
常见用途,代替return,在不太方便写返回值的方法中
概述:
1.在方法体中使用,主动在方法中抛出异常
2.每次只能抛出确定的某一个异常对象
表示明确的在方法的这个位置,这行代码,抛出一个异常对象,程序执行到这一行后,相当于这一行产生了异常,如果不做处理,就向上抛出,然后这一行后面的代码就不能执行了
基本语法:
throw 异常对象;
注意:
- 每次只能抛出一个异常对象
- 一旦程序运行到该throw代码,必然会抛出一个异常对象
- 在方法中抛出一个异常,相当于使用了return关键字
- 方法立刻结束,后面也不能有其它代码了
- throw必须位于方法的最后一行
- 每个异常类的构造方法都可以显式得传入一个字符串,表示异常信息(原因)的说明
- 如果在方法中显式地抛了一个编译时异常
- 那么会和产生编译时异常的代码一样,需要显式处理
- 最好和throws一起使用(如果try…catch就没有意义)
- 一旦主动使用throw关键字,就代表在当前方法中,必然不会处理该异常,此时直接抛给方法调用者去处理
- 举例:
- 结合成员变量的封装和set方法,判断输入的参数是否合法(IllegalArgumentException)
- 空接口(Cloneable)
private static void testThrowCheckableDemo() throws CloneNotSupportedException {
throw new CloneNotSupportedException("发生了禁止克隆异常");
}
private static void testThrowRuntimeDemo() throws NullPointerException{
throw new NullPointerException("发生了空指针异常");
}
牛刀小试
- 编译期异常和运行期异常的区别?
- 需要在编译时期显式处理: 编译时异常
- 不需要在编译时期处理的: 运行时异常
throws和throw的区别
- throws
1.用在方法声明后面,跟的是异常类名
2.可以跟多个异常类名,用逗号隔开
3.表示抛出异常,由该方法的调用者来处理
4.throws表示出现异常的一种可能性,并不一定会发生这些异常
- throw
1.用在方法体内,跟的是异常对象名
2.只能抛出一个异常对象
3.表示抛出异常,可以由方法体内的语句处理(多此一举) 最常见的是结合throws抛给调用者去处理
4.throw则是抛出了异常,执行throw则一定抛出了某种异常
到底是该try…catch还是该抛出
总结一下,目前为止,我们所学习过的异常的处理策略主要有两种:
-
捕获并处理 try -catch
-
向上抛出
- 运行时异常会自动上抛,直到抛给JVM
- 编译时异常需要用throws关键字向上抛出
-
那么究竟,在遇到异常时我们该如何选择处理策略呢?
-
原则:
对于运行时异常,我们不应该写出产生这种异常的代码,应该在代码的测试阶段修正代码。
对于编译时异常,功能内部能够处理的就处理,如果不能够或者没有必要处理,就抛出。
为什么不能子类抛出更多编译时异常?
因为方法有多态现象,如果允许,可能会出现父类指向的显式没有异常,但是实际上子类出现异常
finally
finally的特点
- 无论try中是否发生异常,都会执行
- try-catch代码中有return也不能阻止它
- 特殊情况:在执行到finally之前jvm退出了
- System.exit(0)
finally的作用
- 利用finally代码块无论如何都要执行的特点
- 所以经常用于释放资源,在IO流操作和数据库操作、网络编程等需要操作额外资源的场景中十分常见
例如
private static void method() {
try {
System.out.println(10/0);
}
catch (ArithmeticException e){
e.printStackTrace();
System.exit(0);
}
finally {
System.out.println("finally");
}
一些奇思妙想
- try代码块如果有return
- 程序会先执行完finally代码块,回过头执行try中的return
- catch代码块中如果有return,并且catch正常捕获异常执行
- 程序会先执行完finally代码块后,再回去执行catch中return,从catch代码块中结束方法
- finally代码中有return
- 不会影响finally代码块执行
- 如果finally和catch中都有return
- 程序会直接从finally代码块中的return结束方法
- 如果try中的异常不能正常捕获,但是finally中有return
- 注意此时程序会跳过这个异常,不会抛出异常给JVM报错
- try中的return语句return的是一个方法,但是这个方法又产生了异常
- 自己测试一下吧哈哈
try…catch变形
- try…catch…finally
- try…catch…
- try…catch…catch…
- try…catch…catch…finally
- 以上都是常见形式
- try…finally
- 该形式在不需要捕获异常,但是希望使用finally代码块的场景中使用
注意事项
- 普遍来说,finally需要放在一个catch代码块后面,但这不是必须的,try…finally的结构也是合法的
- 如果某处代码产生异常,但是我不希望在这里立刻处理异常,而是希望抛给调用者
- 但是,我又希望不管有没有产生异常,都要做一系列操作
- 那么选择try…finally结构是可行的,可以实现需求
final,finally和finalize的区别
- final关键字,最终的,最后的。可以修饰类 成员变量 成员方法
- 修饰类,该类不能被继承
- 修饰变量表示一个常量
- 修饰方法表示无法重写的方法
- finally代码块,和try…catch一起使用,具有必然执行的特点
- 异常处理体系当中,用于资源释放
- finalize方法,Object()类中的成员方法,用于做对象销毁的善后工作,释放额外的系统资源
- 由于GC的自动回收不确定性,该方法实际无效
自定义异常
定义一个类,然后让它
自定义异常是程序员自己写的异常,我们具体知道异常在什么地方抛出,这样可以有针对性地找出异常产生原因