目录
没有花里胡哨, 纯干货, 小白也能看懂的异常最详细总结!!
Java 中异常的那点事
引入
异常概念
异常体系
异常分类
一个栗子
两种方式去处理这个例子中的异常
第一种方法
第二种方法
了解一下错误 Error
异常产生过程解析
异常的处理
java 异常处理的五个关键字: try,catch,finally,throw,throws
- throw
- throws(异常处理的第一种方式)
- try{
- }catch(){
- }(异常处理的第二种方式)
- finally
异常处理注意事项 1
1, 多个异常分别处理.
2, 多个异常一次捕获, 多次处理.
3, 多个异常一次捕获, 一次处理.
异常处理注意事项 2
异常处理注意事项 3
自定义异常类
为什么要自定义异常类:
什么是自定义异常类:
异常类如何定义:
一个栗子
自定义异常类的练习
补充知识
Throwable 类中的三个处理异常的方法
Objects 非空判断
Java 中异常的那点事
引入
在理想的状态下, 用户输入数据的格式永远都是正确的, 选择打开的文件也一定存在, 并且永远不会出现 bug. 然而, 在现实世界中却充满了不良的数据和带有问题的代码.
如果一个用户在运行程序期间, 由于程序的错误或一些外部环境的影响造成用户数据的丢失, 用户就有可能不再使用这个程序了. 为了避免这类事情的发生, 至少应该做到以下几点:
向用户通告错误;
保存所有工作结果;
允许用户以妥善的形式退出程序.
Java 使用一种称为异常处理 (exception handing) 的错误捕获机制处理.
异常概念
异常指的是什么?
异常字面上就是不正常的意思.
在程序中的意思就是
异常: 即指在程序执行的过程中, 出现非正常情况, 最终导致 JVM 的非正常停止.
在 Java 等面向对象的编程语言中, 异常本身是一个类, 产生异常就是创建一个异常对象并抛出一个异常对象. Java 虚拟机处理异常的方式就是中断处理.
异常指的不是语法错误, 语法错误时, 编译不通过, 不会产生字节码文件, 根本不能运行.
异常体系
异常机制是在帮我们找到程序中的问题
异常的根类是 java.lang.Throwable
这个根类下有两个子类, 分别是 java.lang.Error java.lang.Exception, 平常我们所说的异常即 java.lang.Exception.
Throwable 体系
1,Error: 严重错误 Error, 无法处理, 只能事先避免, 相当于绝症这种无法治愈的问题. 必须修改源代码, 程序才能继续执行.
2,Exception: 表示异常, 异常产生后, 程序员可以通过代码去纠正, 使得程序继续去运行, 相当于感冒发烧这种小毛病, 进行处理后可以恢复.
异常分类
java.lang.Throwable 类是 Java 中所有异常或者错误的超类.
一般 Exception 指的是编译期异常, 进行编译(写代码时)java 程序出现的问题.
其中 Exception 下有一个特殊的子类: RuntimeException 指的是运行期异常. 即程序运行的时候抛出的异常.
一个栗子
Demo1
产生了编译期异常:
两种方式去处理这个例子中的异常
第一种方法
throws 关键字
通过 throws 关键字声明抛出这个异常, 交给方法的调用者去处理, 在这里 main 方法的调用者是 JVM, 即交给 JVM 去处理.
添加了 throws ParseException 后, 此时发现红线没有了, 程序可以正常执行了.
注意: 此时我们的 "2022-01-01" 与它的 "yyyy-MM-dd" 格式是一致的, 所以只要解决编译时的异常就可以正常执行程序.
那么当我们把格式改成不一致的时候, 即格式不匹配, 比如给它一个 "2022-0101", 它还会抛出异常.
因为此时我们使用的是第一种处理异常的方式, 即交给 JVM 虚拟机去处理, 而虚拟机处理的方式就是中断程序, 并把异常打印出来, 所以出现异常的语句后面的语句就无法执行了, 若我们想让出现异常的语句后的语句依然继续执行, 我们需要来了解第二种异常处理方式.
第二种方法
try{可能会出现异常的代码
} catch(Exception e){异常的处理逻辑}
此时可以看到, 除了打印了异常的信息也执行了后续的代码
再来看一下运行期异常:
此代码编译时并不会有错误提醒, 但是在运行中, 很明显会产生索引越界异常 , 这就是运行期异常. 我们依然可以使用 try,catch 来处理这个异常. 处理之后, 依然可以执行后续代码.
了解一下错误 Error
当空间数为 1024 时, 此时是没有问题的, 也可以执行到后续代码.
但是当我们把空间数增加为 1024*1024*1024 时
此时出现了一个以 Error 结尾的 OutOfMemoryError, 这就是一个错误, 名称为内存溢出错误, 即创建的数组太大, 超出了给 JVM 分配的内存. 产生错误必须修改源代码, 否则是不会继续执行下去的, 在这里即把数组修改的小一点就可以了.
异常产生过程解析
再来看一个例子:
因为我定义的数组下标最大为 2, 很明显, 此时会产生异常
程序执行的结果:
仔细观察, 我们可以发现, 异常是在 int ele = arr[index]; 这一行代码产生的
这时访问了数组中的 3 索引(下标), 但是数组中并没有 3 索引, 这时 JVM 就会检测出程序出现了异常.
1,
JVM 会做两件事:
1)JVM 会根据异常产生的原因创建一个异常对象, 这个异常对象包含了异常产生的(内容, 原因, 位置) new ArrayIndexOutOfBoundsException("3");
2)在 getElement 方法中, 没有异常的处理逻辑(try,catch), 那么 JVM 就会把异常对象抛出给方法的调用者, 也就是让 main 方法来处理异常
getElement 方法把异常对象抛出给 main 方法
2,
回到 main 方法中的这行语句, int e =getElement(arr,3);
main 方法接收到了这个异常对象(new ArrayIndexOutOfBoundsException("3")), 但是 main 方法也没有异常的处理逻辑, 继续把对象抛出给 main 方法的调用者, 即 JVM 处理
main 方法把异常对象抛出给 JVM
3,
JVM 接收到了这个异常对象(new ArrayIndexOutOfBoundsException("3")), 做了两件事情:
1, 把异常对象 (内容, 原因, 位置) 以红色的字体打印在控制台
2,JVM 会终止当前正在执行的 java 程序 -->中断处理
异常的处理
java 异常处理的五个关键字: try,catch,finally,throw,throws
接下来我们挨个来介绍:
throw
关于 throw 关键字的介绍
作用: 使用 throw 关键字可以在指定方法中抛出指定的异常
使用格式:
throw new xxxException("异常产生的原因");
注意事项:
1,throw 关键字必须写在方法的内部
2,throw 关键字后边 new 的对象必须是 Exception 或者 Exception 的子类对象
3,throw 关键字抛出指定的异常对象, 我们就必须处理这个异常对象
throw 关键字后边创建的是 RuntimeException 或者是 RuntimeException 的子类对象我们可以不处理, 默认交给 JVM 去处理
throw 关键字后边创建的是编译器异常, 我们就必须处理这个异常, 要么 throws, 要么 try...catch
我们依然用一个例子来解释它:
执行结果:
这是我并没有处理这个异常, 那它是谁处理的呢?
上边我们提到了
throw 关键字后边创建的是 RuntimeException 或者是 RuntimeException 的子类对象我们可以不处理, 默认交给 JVM 去处理
此时的 NullPointerException 就是一个运行期异常, 即 RuntimeException 的子类, 我们不用处理, 默认交给 JVM 去处理
小 tips:
在工作中, 我们首先必须对方法传递过来的参数做合法性校验
如果参数不合法, 那么我们就必须要使用抛出异常的方式, 告诉方法的调用者, 传递的参数有问题
在上边的例子中我们判断了数组 arr 的值是否为空, 我们还有另外一个参数, 即 index, 我们接着再来对 index 进行合法性校验.
把上边的空数组改为
此时若传递参数为(arr,3)
会抛出 ArrayIndexOutOfBoundsException, 即数组索引越界异常
ArrayIndexOutOfBoundsException 也是一个运行时异常, 默认交给 JVM 去处理.
throws(异常处理的第一种方式)
此种方法即声明异常
throws 关键字: 是异常处理的第一种方式, 即交给别人去处理
作用:
当方法内部抛出异常对象时, 我们就必须处理这个异常对象
可以使用 throws 关键字进行异常处理, 会把异常对象抛出给方法的调用者处理 (自己不处理, 交给别人处理), 若没人处理, 最终交给 JVM 处理 --> 中断处理
使用格式: 在方法声明时使用
修饰符 返回值类型 方法名(参数列表)throws AAAException,BBBException...{
- throw new AAAException("产生异常的原因");
- throw new BBBException("产生异常的原因");
- ....
- }
注意事项:
1,throws 关键字必须写在方法声明处
2,throws 关键字后边的异常必须是 Exception 或者 Exception 的子类
3, 方法内部如果抛出了多个异常对象, throws 后面也必须声明多个异常
如果抛出的异常有子父类关系, 只需声明父类异常即可
4, 调用一个声明异常的方法, 就必须处理声明的异常
如何处理: 1)继续使用 throws 关键字进行声明抛出, 交给方法的调用者处理, 最终交给 JVM 处理
2)要么 try...catch 自己处理异常
举个栗子
定义一个方法对传递的文件路径进行一个合法性判断
如果路径不是 "c:\\.java.txt" 我们就抛出文件找不到这个异常( ), 告诉方法的调用者
此时发现程序已经标了红线, 原因是 FileNotFoundException 是编译器异常, 上面我们说过, 只要出现编译期异常, 我们就必须进行处理
此时就可以使用 throws 关键字继续声明抛出 FileNotFoundException 这个异常对象, 让方法的调用者来处理.
接着我们来补全 main 方法来调用 readFile 方法
此时我传给 readFile 的是正确的路径, 但是发现 readFile 仍然下边依然有红线 , 这是因为我们刚才介绍的注意事项的第四点
那我们就得在 main 来处理这个异常 , 我们依旧使用第一种方法, 即继续使用 throws 关键字进行声明抛出, 此时 main 方法把异常对象交给它的调用者处理, 即让 JVM 去处理.
- public class Demo5 {
- public static void main(String[] args) throws FileNotFoundException {
- readFile("c:\\.java.txt");
- }
- public static void readFile (String fileName)throws FileNotFoundException{
- if (!fileName.equals("c:\\.java.txt")){
- throw new FileNotFoundException("传递的文件路径不是 c:\\.java.txt");
- }
- System.out.println("路径没有问题, 读取文件");
- }
- }
此时代码就没有问题了
我们再来加一个 if 语句, 如果传递的路径不是. txt 结尾
我们抛出 IO 异常对象, 告诉方法的调用者, 文件的后缀名不对
此时我们把传递的文件路径后缀名改为. tx, 它就会报 IO 异常, 我们要像上边声明抛出 FileNotFoundException 异常对象一样, 声明抛出 IOException 异常对象
注意: 由于 FileNotFoundException 是 IOException 的子类, 所以只需声明抛出 IOException, 即父类异常即可!!
try{}catch(){}(异常处理的第二种方式)
此种方法即捕获异常
上边我们介绍过的第一种异常处理方式 - 声明异常, 不难发现, 它是有一定缺陷的.
如果我们在上面 Demo5 的例子中, 给 main 方法中的 readFile("c:\\.java.tx");
这条语句后边加一个
System.out.println("后续代码");
即让程序执行后续代码, 发现后续代码是不能执行的. 原因也很简单, 就是我们上边讲过的, 若没人去处理这个异常, 最后会交给 JVM 去处理, 而 JVM 处理的方式是中断程序, 所以后续代码自然就不能执行了.
而 try...catch 是自己去处理异常, 后续代码也可以继续执行.
try...catch, 异常处理的第二种方式, 自己处理异常
格式:(一个 try 中可以对应多个 catch)
try{可能产生异常的代码
} catch(定义一个异常的变量, 用来接收 try 中抛出的异常对象){
异常的处理逻辑, 产生异常之后, 怎么处理异常对象
一般在工作中, 会把异常信息记录在日志中
- }
- ...
- catch(异常类名 变量名){
- }
注意事项:
1,try 中可能会出现多个异常对象, 可以使用多个 catch 来处理这些异常对象
2, 如果 try 中产生了异常, 那么就会执行 catch 中的异常处理逻辑, 执行完 catch 中的异常处理逻辑, 继续执行 try...catch 后的代码
如果 try 中没有产生异常, 那么不执行 catch 的异常处理逻辑, 即执行完 try 中的语句, 继续处理 try...catch 后的代码
举个栗子
依然是上边的文件路径的例子, 只是 此时我们的 main 方法在收到 readFile 传递的异常对象之后, 不再声明抛出给 JVM 来处理, 而是使用 try...catch 自己进行处理.
当传递的参数为正确的文件路径时, 此时, 程序没有异常产生, 不执行 catch 中的异常处理逻辑, 程序正常执行.
此时打印:
当传的参数为错误的文件路径时, 此时, 程序有异常产生, catch 捕捉到 try 中产生的异常, 并执行了异常处理逻辑, 执行完 catch 后, 程序依然继续执行后续代码(不同于 throws 的地方).
此时打印:
finally
finally: 有一些特定的代码无论异常是否发生, 都需要执行. 另外, 因为异常会导致程序跳转, 导致有些语句执行不到. finally 就是用来解决这个问题的, 放在 finally 中的语句块一定会被执行到.
我们写在 try 中的代码如果出现了异常, 就会直接把异常抛给 catch 来处理, 那么在 try 中出现异常的位置之后的代码就是执行不到的.
如图:
此时我想打印这个 "释放空间", 是执行不到的, 因为产生了异常, 直接跳到了 catch 语句中 .
如果我想把 "释放空间" 打印出来, 此时就可以使用 finally 语句.
finally 代码块:
格式:
try{可能产生异常的代码
} catch(定义一个异常的变量, 用来接收 try 中抛出的异常对象){
异常的处理逻辑, 产生异常之后, 怎么处理异常对象
一般在工作中, 会把异常信息记录在日志中
- }
- ...
- catch(异常类名 变量名){
- }finally{
无论是否出现异常都会执行}
注意事项:
1,finally 必须和 try 一起使用, 不能单独使用
2,finally 一般用于资源释放(资源回收), 无论程序是否出现异常, 最后都要资源释放
异常处理注意事项 1
多个异常如何捕获与处理?
共有三种方法:
1, 多个异常分别处理.
2, 多个异常一次捕获, 多次处理.
3, 多个异常一次捕获, 一次处理.
1, 多个异常分别处理.
即有一个异常就要写一个 try...catch
即格式为
- try{
- }catch(){
- }
- try{
- catch(){
- }
- System.out.println("后续代码")
此种方式有个优点: 就是可以执行到后续代码
2, 多个异常一次捕获, 多次处理.
即一个 try 对应多个 catch
即格式为
- try{
- }catch(){
- ......
- } catch() {
- }
此种方法使用时要注意:
catch 里边定义的异常变量, 如果有子父类关系, 那么包含子类异常变量的 catch 语句必须写在父类的上边, 否则会报错.
例如如图的情况, 就报错了.
原因是:
例如: try 中可能会产生以下两个异常对象:
- new ArrayIndexOutOfBoundsException("3");
- new IndexOutOfBoundsException("3");
try 中如果出现了异常对象, 会把异常对象抛出给 catch 处理
抛出的异常对象, 会从上到下赋值给 catch 中定义的异常变量.
如果父类异常变量的 catch 语句在子类的上边, 此时无论是产生子类异常还是产生父类异常, 都会赋给父类 catch 语句中的异常变量(多态的体现), 而下边子类 catch 语句中的异常变量就没有被使用所以会报错, 这并不是我们想要的结果.
所以, 在 catch 里边定义的异常变量, 如果有子父类关系, 那么包含子类异常变量的 catch 语句必须写在父类的上边.
3, 多个异常一次捕获, 一次处理.
即只有一个 try 和一个 catch
即格式为
- try{
- }catch(此时这里的异常变亮一般为父类异常即可以处理多个异常对象或者直接写 Exception){
- }
特殊的: 运行时异常 (RuntimeException) 可以不处理也不声明抛出
默认交给虚拟机去处理, 终止程序, 什么时候不抛出运行时异常了, 再执行程序.
异常处理注意事项 2
如果 finally 中有 return 语句, 永远返回 finally 中的结果, 我们应该避免该种情况.
- public class Demo6 {
- public static void main(String[] args) {
- int a =getA();
- System.out.println(a);
- }
- public static int getA(){
- int a = 10;
- try{return a;
- }catch(Exception e){
- System.out.println(e);
- }finally {
- a = 100;
- return a;
- }
- }
- }
此时打印结果为 100, 我们应该去避免这种情况的发生, 即不在 finally 里写 return 语句.
异常处理注意事项 3
关于子父类的异常问题
(此部分代码较简单, 不进行演示, 只要记住下边两条, 自然就会使用了)
1)如果父类抛出了多个异常, 子类重写父类方法时, 抛出和父类相同的异常, 或者是父类异常的子类或者是不抛出异常.
2)父类方法没有抛出异常, 子类重写父类该方法时也不可抛出异常, 此时子类产生该异常, 只能捕获处理, 而不能声明抛出.
自定义异常类
为什么要自定义异常类:
Java 中的不同的异常类, 分别表示着某一种具体的异常情况. 但是在具体的开发过程中, 我们总会用到一些 Java 中没有的异常类, 比如我们要考虑考试成绩是负数的问题. 这时就需要我们自己去定义一个异常类.
什么是自定义异常类:
在开发中自己业务的异常情况来定义异常类.
自定义一个业务逻辑异常: RegisterException, 即一个注册异常类.
异常类如何定义:
1, 自定义一个编译期异常: 自定义类并继承于 java.lang.Exception.
2, 自定义一个运行时期的异常类: 自定义类并继承于 java.lang.RuntimeException
格式:
public class Exception extends Exception/RuntimeExcetion{
添加一个空参数的构造方法
添加一个带异常信息的构造方法
}
注意:
1, 自定义异常类一般都是以 Exception 结尾, 说明该类是一个异常类
2, 自定义异常类, 必须得继承自 Exception 或者 RuntimeException
继承自 Exception: 那么定义的异常类就是一个编译期异常, 如果方法内部抛出了编译期异常, 就必须处理这个异常, 要么 throws , 要么 try ...catch
继承自 RuntimeException: 那么定义的异常就是一个运行期异常, 无需处理, 交给虚拟机处理(中断处理)
下面我们来自定义一个异常类
一个栗子
- public class RegisterException extends Exception {
- // 添加一个空参数的构造方法
- // public RegisterException(){}
- public RegisterException(){super();}
- // 实际上我们此时默认调用的是空参的父类的构造方法, 以上两条语句等价
- /* 添加一个带异常信息的构造方法, 这个怎么添加呢
- 我们可以参照一下 jdk 中的 NullpointerException 源码中的构造方法
- 查看 NullpointerException 的源码后发现, 所有异常类都会有一个带异常信息的构造方法
- 在方法内部会调用父类带异常信息的构造方法, 让父类来处理这个异常信息 */
- public RegisterException (String message){
- super(message);
- }
- }
自定义异常类的练习
我们使用上面我们定义好的异常类 RegisterException 进行练习
- import java.util.Scanner;
- /* 要求: 模拟注册操作, 如果用户名已存在, 抛出异常并提示, 该用户名已被注册.
- 分析:
- 1, 使用数组保存注册过的用户名
- 2, 使用 Scanner 获取用户输入的注册的用户名
- 3, 定义一个方法, 对用户输入的注册的用户名进行判断
- 遍历存储已经注册过用户名的数组, 获取每一个用户名
- 使用获取到的用户名和用户输入的用户名比较
- true:
- 用户名已经存在, 抛出 RegisterException, 告知用户该用户名已经注册
- false:
- 继续遍历比较
- 如果循环结束, 还没找到重复的, 提示用户, 注册成功!
- */
- public class RegisterException2 {
- static String[] usernames = {"张三","李四","王五"};
- public static void main(String[] args) throws RegisterException {
- Scanner sc = new Scanner(System.in);
- System.out.println("请输入你要注册的用户名");
- String username = sc.next();
- checkUsername(username);
- }
- public static void checkUsername(String username) throws RegisterException {
- for (String name:usernames) {
- if (name.equals(username)){
- throw new RegisterException("该用户名已经注册");
- }
- }
- System.out.println("注册成功!");
- }
- }
- }
此时, 我们输入不存在的用户名, 运行结果如下
输入已经存在的用户名, 运行结果如下
达到了我们想要的结果.
上边使用的是 throws 一直声明抛出, 最终交给了 JVM 去处理.
我们也可以使用 try... catch 来处理
- public class RegisterException2 {
- static String[] usernames = {"张三","李四","王五"};
- public static void main(String[] args) {
- Scanner sc = new Scanner(System.in);
- System.out.println("请输入你要注册的用户名");
- String username = sc.next();
- checkUsername(username);
- }
- public static void checkUsername(String username) {
- for (String name:usernames) {
- if (name.equals(username)){
- try {
- throw new RegisterException("该用户名已经注册");
- } catch (RegisterException e) {
- e.printStackTrace();
- }
- }
- }
- System.out.println("注册成功!");
- }
- }
运行结果如下
此时我们发现了一个问题, 即当我输入已存在的用户名, 程序抛出异常后, 依然会打印注册成功, 这显然不符合预想.
如果抛出了异常, 我们应该让方法停下来, 不再继续执行后面的语句
这里只需要在 catch 里添加一个 return 即可.
刚才我们继承的是 Exception, 现在让我们自定义的 RegisterException 再来继承一下 RuntimeException.
继承之后, 发现及时不加 try..catch 语句, 也不声明, 程序也不会报错, 原因很简单, 就是我们在上边一直在讲的, 抛出 RuntimeException(运行期异常)可以不处理, 默认交给 JVM 来处理(中断程序).
运行结果
补充知识
Throwable 类中的三个处理异常的方法
Throwable 类中定义了三个处理异常的方法
分别是以下三个:
String | getMessage () |
String | toString () |
void | printStackTrace()
JVM 打印异常对象默认使用此方法,打印的异常 信息是最全面的
|
举个栗子
我们分别来打印它们进行观察
依然使用上边的代码 Demo5 例子, 我们此时给它传递一个错误的文件路径(后缀名是错误的). 我们来看一下三种处理异常的方法的区别.
1)get Message 方法
此时打印:
可以看到只有很简短的描述
2)toString 方法
此时打印:
可以看到比上边的 getMessage 方法详细了一点
3)printStackTrace 方法
此时打印:
可以看到此时打印了最详细的异常信息.
Objects 非空判断
Objects 类是一个由一些静态的实用方法组成的类, 这些方法是 non-save(空指针安全的)或 non-tolerant(容忍空指针的), 那么在它的源码中, 对对象为 null 的值进行了抛异常操作.
Objects 类中的静态方法
public static <T> T requireNonNull(T obj): 查看指定引用对象不是 null
源码:
- public static <T> T requireNonNull(T obj){
- if(obj == null){
- throw new NullPointerException();
- return obj;
- }
举个栗子:
上边的代码可对传过来参数进行合法性校验, 判断其是否为空
当我们了解了 Objects 类的 requireNonNull 方法后, 可对代码进行一个简化
即把注释掉的这两行 if 语句替换成了 Objects.requireNonNull(obj);
以后如果我们在合法性判断时, 如果要判断它是否为空, 可以直接用 Objects 类里的静态方法即 requireNonNull, 可以简化书写代码.
到这里, 异常部分全部介绍完毕, 本人才疏学浅, 若各位发现错误, 请尽情批评指正!!!
来源: https://blog.csdn.net/m0_63216020/article/details/122420099