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 () 返回此throwable的简短描述。 |
---|---|
String | getMessage () 返回此throwable的简短描述。 |
String | toString () 返回此throwable的详细消息字符串 |
---|---|
String | toString () 返回此throwable的详细消息字符串 |
void | printStackTrace () 将此throwable及其追踪输出至标准错误流。JVM打印异常对象默认使用此方法,打印的异常信息是最全面的 |
---|---|
void | printStackTrace () 将此throwable及其追踪输出至标准错误流。JVM打印异常对象默认使用此方法,打印的异常信息是最全面的 |
举个栗子
我们分别来打印它们进行观察
依然使用上边的代码Demo5例子,我们此时给它传递一个错误的文件路径(后缀名是错误的)。我们来看一下三种处理异常的方法的区别。
1)get Message方法
此时打印:
可以看到只有很简短的描述
2)toString 方法
此时打印:
可以看到比上边的getMessage方法详细了一点
3)printStackTrace方法
此时打印:
可以看到此时打印了最详细的异常信息。
Objects非空判断
Objects类是一个由一些静态的实用方法组成的类,这些方法是non-save(空指针安全的)或non-tolerant(容忍空指针的),那么在它的源码中,对对象为null的值进行了抛异常操作。
Objects类中的静态方法
public static
源码:
public static
T requireNonNull(T obj){ if(obj == null){
throw new NullPointerException();
return obj;
}
举个栗子:
上边的代码可对传过来参数进行合法性校验,判断其是否为空
当我们了解了 Objects类的requireNonNull方法后,可对代码进行一个简化
即把注释掉的这两行if语句替换成了Objects.requireNonNull(obj);
以后如果我们在合法性判断时,如果要判断它是否为空,可以直接用Objects类里的静态方法即requireNonNull,可以简化书写代码。
到这里,异常部分全部介绍完毕,本人才疏学浅,若各位发现错误,请尽情批评指正!!!
若对您有帮助,请点赞,收藏,加关注!!!我们一起努力!!谢谢!!!