目录
一、前言
大家好,今天给大家带来一篇java 异常基础的万字详解,希望通过举栗和讲解,帮助初学者快速了解并能上手java异常,以便继续学习javaFile类,javaIO流和java反射,注意代码中的注释也很重要。up的所有文章都会适时修改和补充,以更完善。良工不示人以朴,感谢阅读!(PS:点击目录可以跳转)。
二、异常概述:
①异常,即程序运行过程中出现的非正常情况,通俗地说,异常就是程序出现的非语法错误,最终会导致JVM的非正常停止。
②异常并不是语法错误,语法错了,编译不通过,不会产生字节码文件,压根不能运行。
三、异常分类:
1.前言
java中Throwable类是所有异常和错误的超类(父类),即异常的顶层父类。Throwable类属于java.base模块,java.lang包下。它下面有两个子类Error和Exception,前者是错误,后者就是我们常说的异常🌶。来张图直观一点:
2.异常(Exception):
①概述:
指合理的应用程序可能需要捕获的问题,是可以通过常用处理机制来解决的。
eg:NullPointerException(空指针异常)
ArithmeticException(运算条件异常)
②编译期异常和运行期异常:
Δ异常又可以分为编译期异常和运行期异常:
编译期异常,即进行编译时出现的问题。我们必须采取相应的处理机制。
运行期异常,即代码运行中出现的问题。我们可以不处理,默认交给JVM来处理。说到运行期异常就不得不说到Exception类下面的一个子类,RuntimeException。RuntimeException和RuntimeException下的所有子类异常都属于运行期异常。来张图看看RuntimeException下的子类究竟有多少,如下图:
查询API可以得知,直接已知的子类数目已经相当惊人!其实,我们平时常见的(运行期)异常,很多都是RuntimeException的子类,眼尖的小伙伴儿们估计也能在图里面发现几个熟人了。
③代码演示:
我们先在exception包下创建一个IntroException类作为演示类,如图:
IntroException类代码如下:
packageknowledge.exception; importjava.io.FileNotFoundException; importjava.io.FileReader; importjava.io.Reader; publicclassIntroException { publicstaticvoidmain(String[] args) throwsFileNotFoundException { //异常(Exception)举栗://1) 编译期异常举栗:Readerreader=newFileReader("D:/JAVA/IDEA/file/1.txt"); /*此处就有异常提示,未处理的异常:java.io.FileNotFoundException.(找不到文件异常)出现该异常的原因是程序认为你传入的路径可能是瞎写的。可通过抛出异常解决。假如不匹配,就是JVM的默认处理方式了*///2) 运行期异常举栗://这里我们先将代码写到了try...catch语句的外面,不对它进行处理。int[] array=newint[4]; System.out.println(array[4]); /*第45行会在运行时报ArrayIndexOutOfBoundsException,即数组下标越界异常,这是因为该数组长度为4,索引范围是0~3,不可能出现4的索引。因此代码执行到这里时就会报异常。 *//*(try...catch...可以捕获异常,我们后面会讲到,先让大家认识一下)*/try { //可能会出现异常的代码System.out.println("👴非要看看第五个元素:"+array[4]); } catch (Exceptione) { //异常的处理逻辑System.out.println(e); } finally { System.out.println("非要看不存在的元素?"); } } }
注意,对于编译期异常,只要出现,编译器就会即时报出异常,表现为用红色的波浪线标注出来,如下图所示:
这时候代码是不能运行的,因为只要你按下运行键,控制台会显示提示: 必须对这个编译期异常作出相应的处理机制。如下图:
所以, 我们得到结论,出现编译期异常时,我们必须进行相应的处理,否则程序无法运行。此处我们直接抛出异常,即在方法声明处加上throws + 要抛出的异常的名字。如下图所示:
接下里我们把 Reader reader = new FileReader("D:/JAVA/IDEA/file/1.txt"); 这句代码注释掉,看看运行期异常是怎么回事,运行结果如下图:
JVM也是毫不留情的告诉我们出现异常了,它打印的是抛出的异常对象,内容:ArrayIndexOutOfBoundsException(数组下标越界异常) ,原因:索引4超过了长度为4的数组的索引范围。位置: main方法中的第52行出现了问题。(后面异常的产生机制中会详细解释)
接着我们把出问题的代码放在try...catch语句中看看会发生什么?(try...catch...可以捕获异常,我们后面会讲到,先让大家认识一下)如下gif图:
运行结果:
可以看到,经过我们处理后,JVM没有再用红色的字体在控制台打印出异常对象。
3.错误(Error):
①概述:
指合理的应用程序不应该试图捕获的,严重的问题,是不可以通过常用处理机制来解决的。
eg1:StackOverFlowError(栈内存溢出)
eg2:OutOfMemoryError(内存溢出)
Δ发现了没有?
异常以Exception作结尾,而错误以Error作结尾。
②代码演示:
我们先在exception包下,创建一个IntroError类作为演示类,如图:
IntroError类代码如下:
packageknowledge.exception; importjava.io.FileNotFoundException; publicclassIntroError { publicstaticvoidmain(String[] args) throwsFileNotFoundException { //错误(Error)举栗:int[] arrayTest=newint[1024*1024*1024]; /*OutOfMemoryError: java heap space,注意结尾是Error了.内存溢出的错误,数组所占的内存太大,超出了给JVM分配的内存解决方法:必须修改代码中引起错误的部分*/System.out.println("必须修改掉代码中引起错误的部分,这句话才能成功打印出来。"); System.out.println("----------------------------------------"); } }
注意,这里我们定义了一个长度巨**离谱的数组,显然这会对内存造成负担,让我们看看运行结果如何,如图:
噢,他提示我们内存溢出了,而原因是堆空间溢出了,这是因为数组通过new关键字,在堆空间中创建新的内容,堆空间是有限的,我们申请的数组长度太长,当然会溢出。而数组名本身不过是一个指向罢了,或者叫做引用,指向堆中申请的这片空间。
接下来我们把数组长度改小,看看情况会不会好一点儿,如图gif:
显然,在数组长度[1024 * 1024 * 1024] 中去掉一个1024后就没有再报错了。
四、异常的产生机制(重要):
异常的产生过程:
1.程序执行到代码异常处时,JVM检测出程序出现异常,这时它会 根据异常产生的原因创建一个异常对象,这个异常对象包含了异常产生的(原因,内容,位置)
2.若异常所在的方法中没有相应的异常处理机制,那么JVM就会把 异常对象抛给方法的调用者(此处为main方法)来处理这个异常
3.main方法接收到了这个异常对象后,若main方法也没有异常处理机制, 就会继续把main方法抛给它的调用者(即JVM)来处理。
4.当JVM收到main方法抛来的这个异常对象后,它会在控制台打印出这个异常对象,并终止程序.
代码演示:
我们先在exception包下创建一个Process类作为演示类,如图:
Process类代码如下:
packageknowledge.exception; importjava.util.Scanner; /**JVM默认的异常处理方式:创建对应异常类的对象,并抛出一个异常对象.在控制台打印异常信息,并终止程序.*/publicclassProcess { publicstaticvoidmain(String[] args) { //1.定义一个数组int[] array= {1, 2, 11, 2, 23}; System.out.println("正常情况下的遍历数组:"); printArr(array); Scannersc=newScanner(System.in); System.out.println("输入你想指定的索引,根据索引获取数组的指定内容:"); intindex=sc.nextInt(); //我们故意输入一个不存在的索引getAssignedElement(array, index); /*先把代码看完,再跟着看注释!第一步,执行到getAssignedElement(array, index);这条代码时,JVM便检测出异常,并创建了一个包含异常内容,原因以及位置的异常对象*//*第二步,若异常所在的方法中没有相应的异常处理机制,那么JVM就会把异常对象抛给方法的调用者(此处为main方法)来处理这个异常*//*第三步,new ArrayIndexOutOfBoundsException("5");main方法接收到了这个异常对象,若main方法也没有异常处理机制,就会继续把main方法抛给它的调用者,即JVM处理*//*第四步,当JVM收到main方法抛来的这个异常对象后,它会在控制台打印出这个异常对象,并终止程序*/sc.close(); } //2.定义一个方法来遍历数组/*标准的i < arr.length , 因此该方法无数组下标越界异常。*/publicstaticvoidprintArr(int[] arr) { for (inti=0; i<arr.length; ++i) { System.out.println(arr[i]); } } //3.定义一个方法来获取数组指定索引处的元素。publicstaticvoidgetAssignedElement(int [] arr, intindex) { System.out.println(arr[index]); /*假设我们传入的索引是5,而我们所定义的数组一共只有五个元素,索引是0~4,是没有5这个索引的,所以运行到这里时,JVM就会检测处程序出现异常。这时,JVM会做两件事:1) JVM会根据异常产生的原因创建一个异常对象,这个异常对象包含了异常产生的(原因,内容,位置),eg:new ArrayIndexOutOfBoundsException("5");2) 由于在getAssignedElement方法中没有相应的异常处理机制,那么JVM就会把异常对象抛给方法的调用者,即main方法来处理这个异常*/ } } /*new ArrayIndexOutOfBoundsException("5");当JVM收到 main方法抛来的这个异常对象后,它又要做两件事:1) 把异常对象(内容,原因,位置)以红色的字体打印在控制台2) JVM终止程序eg :ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 5内容:数组下标越界异常原因:咱们指定的索引不合法位置:Process.java:___*/
运行结果如下gif:
图解演示:
五、异常相关:
1.关于throw关键字:
①前言
注意,throw关键字和throws关键字长得像,但可不是一回事儿,不要混淆了。前者是在指定方法中抛出异常对象,而且可以自己更改提示信息,后者则是常用的异常处理方式之一。
了解即可,重要的是把代码都能看懂了。
②作用:
可以使用指定的关键字在指定的方法中抛出指定的异常对象
③使用格式:
throw new ____Exception("异常产生的原因");
PS:其实“异常产生的原因这里”,你可以随便写你想写的提示内容。
④使用throw关键字的三个注意事项:
1) throw关键字必须写在方法的内部
2) new出来的对象必须是Exception 或者 Exception 的子类对象
3) throw关键字抛出指定的异常对象,我们就必须处理这个异常对象
Δ若是运行期异常,即创建的RuntimeException 或 RuntimeException的子类对象, 我们可以不处理,默认交给JVM处理,(打印异常对象并终止程序)
Δ若是编译期异常,即写代码的时候出现了异常,我们就必须处理这个异常,而处理的方式, 要么是throws, 要么是try...catch
⑤代码演示:
我们先在exception包下创建一个ThrowKey类作为演示类,如图:
ThrowKey类代码如下:
packageknowledge.exception; /**实际开发中,我们必须首先对方法传递过来的参数进行合法性校验如果参数不合法,我们就必须通过抛出异常来告知方法的调用者:传递的参数有问题eg: 如下,如果数组指向为空,我们就通过throw new nullPointerException(抛出空指针异常)的方式来通知调用者。*/publicclassThrowKey { publicstaticvoidmain(String[] args) { int[] array1=null; int[] array2= {1, 2, 3, 4, 5}; intelement=getElement(array1, 2); //传入一个空引用作测试intelement2=getElement(array2, 5); //传入一个不存在的索引作测试 } //定义一个方法,用来获取数组中的元素。publicstaticintgetElement(int[] arr, intindex) { //先对数组这个引用作检验:if (arr==null) { thrownewNullPointerException("传过来的数组是空的,检查一下是否传错了!"); /*由于nullPointerException(空指针异常)是运行期异常,因此我们可以不用管,默认交给JVM处理即可。*/ } //再对索引参数进行合法性检验,/*如传入的索引超出了数组索引的范围,我们就可以通过throw new ArrayIndexOutOfBoundsException(抛出数组下标越界异常)的方式来告知方法的调用者:传入的索引不在数组的正常索引范围内。*/if (index>=arr.length||index<0) { thrownewArrayIndexOutOfBoundsException("你干嘛~ 数组的索引最大就是(数组的长度-1)"); /*由于ArrayIndexOutOfBoundsException(数组下标越界异常)是运行期异常,因此我们可以不用管,默认交给JVM处理即可。*/ } returnarr[index]; } }
看一下运行效果:
发现没有?我们通过throw关键字抛出的异常对象成功打印了出来,并且我们写入的提示信息也跟着打印了出来,牛逼!🐂🐂🐂
接下来我们把测试1的代码注释掉,进行测试2:即数组引用正常,但传入的索引不正常,会怎么样?运行效果如下gif图:
可以看到我们的提示信息再次成功打印出来!
2.关于Objects类的静态方法:
①前言:
前面我们说过,在实际开发中,带参方法要对传入的参数进行合法性检验。然而,gosling早就设置了懒人青睐的方法,都给你写好了,你还需要自己写吗?懒人有懒福(bushi)
②介绍:
Objects类属于java.base模块,java.util包下,继承自Object类。我们要用到它里面的静态方法requireNonNull(T obj),该方法可以判断传入的对象是否为空, 常用的还有它的一个重载的方法是requireNonNull(T obj, String message) ,判断是否为空的同时,还可以在判断为空的情况下修改打印出的异常对象的原因(即提示信息)。大家也可以去查看API,我截个图放下面:
可以看到第一个静态方法可以检查指定的对象引用是否为空,第二个它的重载则可以在第一个方法的基础上抛出自定义的空指针异常。
③代码演示:
我们先在exception包下创建一个JudgeNull类作为演示类,如图:
JudgeNull类的代码如下:
packageknowledge.exception; importjava.util.Objects; /** Objects类中用于判断对象是否为空的静态方法:* 源码:* public static <T> T requireNonNull(T obj) {* if (obj == null) {throw new NullPointerException();* }* return obj;* }* //当我们对传入的参数进行合法性校验时,可直接调用此方法,就无需再自己来判断了* */publicclassJudgeNull { publicstaticvoidmain(String[] args) { //1.使用我们以往的方法:testMethod(null); /*运行结果: JVM会打印出对应的异常对象,并且携带有我们的提示信息。*///2.使用Objects类的静态方法:newMethod(null); } publicstaticvoidtestMethod(Objectobj) { //1.以往我们对参数进行合法性检验, 判断它是否为空时:if (obj==null) { thrownewNullPointerException("该引用为空!"); } return; } publicstaticvoidnewMethod(Objectobj) { //2.时代变了,我们可以直接调用Objects类的静态方法:/*需注意:该方法还有两个常用的重载:1)Objects.requireNonNull(obj);2)Objects.requireNonNull(obj, message);//方法2)与 1)相比,多传入一个String类型,表示要打印出的提示信息*///eg:Objects.requireNonNull(obj); //Objects.requireNonNull(obj, "传入对象的引用为空!"); return; } }
1) 我们先来测试一下以往使用throw关键字的常规方法,运行效果如下gif:
2) 再来测试 一下Objects类的静态方法,运行效果如下gif:
3) 最后测试一下Objects的重载方法,效果如下gif图:
六、异常处理(重要):
1.JVM的默认处理方式:
创建对应异常类的对象并抛出一个异常对象,然后JVM会以红色的字体在控制台打印出错误信息,并终止程序。
2.常见的两种处理方式:
①通过throws关键字:
通过throws关键字将异常抛给调用者,最终抛给JVM,JVM做中断处理。
格式:
在方法声明时使用:
修饰符 返回值类型 方法名() throws ___Exception { //throw new XxxException();...... }
注意事项:
Δthrows关键字必须写在方法声明处(与throw作区分)
Δthrows关键字后面声明的异常必须是 Exception 或 Exception的子类
Δ如果抛出的异常有子父类关系,直接声明父类即可(即直接抛出异常的父类),特别适用于出现多个编译期异常且这些异常有统一具体的父类时,比如在java反射或者javaIO流中常见。
Δ若调用的方法抛出了异常,我们就必须处理声明的异常,要么接着抛,直到抛给,JVM,要么就用之后要讲到的try...catch...方法
代码演示:
我们现在exception包下创建一个HandlingOne类作为演示类,如图:
HandlingOne类代码如下:
packageknowledge.exception; importjava.io.FileNotFoundException; importjava.io.FileReader; importjava.io.Reader; publicclassHandlingOne { publicstaticvoidmain(String[] args) throwsException{//接着抛,main 函数会抛给JVM.//需求1: 调用testH() 方法//因为testH方法已经抛出了一个异常,所以作为调用者(main函数) 必须处理这个异常,有两种处理方案//方案一: 接着抛 main函数会抛给JVMtestH(); //方案二: try..catch...(等会儿再讲)//需求2: 调用readData() 方法//readData("D:\\FUCK"); } //定义一个测试方法testH():publicstaticvoidtestH() { inta=211/0; System.out.println("The value of a is :"+a); //会抛出ArithmeticException异常,但ArithmeticException是运行期异常,所以可以不处理 } //再来举个栗子:publicstaticvoidreadData(StringfileName) throwsFileNotFoundException { /*这里针对throw抛出的异常对象,我们选择了继续抛,抛给main方法*///对传入的参数进行合法性检验,判断其是否为d盘下file文件夹里的文件。if (!fileName.startsWith("D:\\JAVA\\IDEA\\file")) { thrownewFileNotFoundException("你传入的路径有可能是瞎🐔8写的!"); /*FileNotFoundException属于编译期异常,因此必须对该异常进行处理。此处我们通过throws关键字抛给该方法的调用者,即抛给了main方法*/ } Readerreader=newFileReader(fileName); //...... } }
I.我们先来测试一下testH() 方法,即常规的参数合法性检验,运行结果如下图:
可以看到当我们试图让除数为0时,JVM在控制台上打印出了ArithmeticException,运算异常。
II.我们再来测试一下readData() 方法,运行效果如下gif图:
②通过try...catch方法:
通过try...catch...方法自己来捕获并处理异常
格式: 代码中的注释更详细
try {
//尝试执行的代码
} catch (Exception) {
......
}.......
注意事项:
1.try中可能会抛出多个异常对象,那么就可以使用多个catch语句来处理这些异常对象
2.如果try中产生了异常,那么就会执行catch语句中的处理代码,反之不执行 try...catch...语句执行完之后,会继续执行之后的代码
代码演示:
我们先在exception包下创建一个HandlingTwo类作为演示类,如图:
HandlingTwo类代码如下:
packageknowledge.exception; importjava.io.BufferedReader; importjava.io.FileReader; importjava.io.IOException; /*常用异常处理方式二:通过try...catch...方法自己来捕获并处理异常格式:try {//尝试执行的代码(可能出现的异常)} catch (Exception e) { //括号中要定义一个异常类型的变量,用来接收try中捕获的异常//异常的处理逻辑,即出现可能的异常之后的处理代码//一般在实际开发中,会将异常的信息记录到一个日志中} catch (异常类型 异常变量) {......//多个catch语句格式一致}......注意事项:1.try中可能会抛出多个异常对象,那么就可以使用多个catch语句来处理这些异常对象2.如果try中产生了异常,那么就会执行catch语句中的处理代码,反之不执行try...catch...语句执行完之后,会继续执行之后的代码*/publicclassHandlingTwo { publicstaticvoidmain(String[] args) { //readText("D:\\JAVA\\IDEA\\file\\1.txt");/*由于调用的方法抛出了IOException,所以直接写会显示:未处理的异常: IOException这次我们不再退缩!直接使用try...catch重拳出击.*/try { //可能出现异常的代码readText("D:\\JAVA\\IDEA\\file\\1.ttt"); } catch (IOExceptionioE) { System.out.println("你传入的路径不对哈"); } System.out.println("只有try...catch...执行完后,这句话才会打印出来!"); } //定义一个方法用来读取一个文本文件的内容publicstaticvoidreadText(StringfileName) throwsIOException{ if (!fileName.endsWith(".txt")) { thrownewIOException("传入的路径不是文本文件!"); /*此处抛出了IOException,是编译期异常,必须对throw抛出的异常进行处理,我们用throws关键字抛给main方法。*/ } System.out.println("嗡嗡,你是对的!"); BufferedReaderbufferedReader=newBufferedReader(newFileReader(fileName)); Stringdata; while ((data=bufferedReader.readLine()) !=null) { System.out.println(data); } } }
I.我们先传入一个不是以".txt"结尾的路径,看看输出情况如何,如下图:
是catch中的语句成功打印出了路径不对的提示。
II.我们再传入一个正确的路径看看输出效果,如下gif图:
成功读取了1.txt文本文件中的内容并打印了出来 。
3.Throwable类中的三个常用异常处理方法:
①三个常用方法:
以下三个方法可用于try...catch...语句中的catch语句里面,作为异常处理逻辑。
I.String getMessage() 返回此throwable的简短描述
II.String toString() 返回此throwable的详细消息字符串
III.void printStackTrace() JVM打印异常对象,默认此方法,打印的异常信息是最全面的。(注意返回值是void类型)
②代码演示:
我们先在complement包下创建一个ThreeWay类作为演示类,如图:
ThreeWay类代码如下:
packageknowledge.exception.complement; importjava.util.Objects; publicclassThreeWay { publicstaticvoidmain(String[] args) { try { int[] array= {1, 2, 3, 4, 5}; getElement(array, 5); /*定义的数组array索引是0~4,因此传入索引5一定会出问题。*/ } catch (ArrayIndexOutOfBoundsExceptionindexE) { System.out.println("测试第一种方法如下:\n"+indexE.getMessage()); System.out.println("测试第二种方法如下:\n"+indexE.toString()); System.out.println("直接打印异常会如何?\n"+indexE); System.out.println("测试第三种方法如下:" ); indexE.printStackTrace(); } } publicstaticintgetElement(int[] arr, intindex) { Objects.requireNonNull(arr, "This pointer is null"); intelement=arr[index]; returnelement; } }
运行结果如下图:
由运行结果我们可以得知:
方法一二三,一个比一个详细。而直接打印异常对象则默认使用 toString() 方法,即返回throwable的详细消息字符串。
4.finally代码块:
finally代码块是对try...catch...语句的补充和完善,finally中的代码一定会执行。
①格式:
try {
//尝试执行的代码
} catch (Exception e) {
//异常的处理逻辑(解决方法)
} finally {
//无论是否出现异常,都会执行的代码
}
②注意事项:
1) finally 不能单独使用,必须与try..catch...语句一起
2) finally 一般用于资源释放(资源回收),无论程序是否出现异常,最后都要释放资源(常见于IO流)。
③代码演示:
我们先在complement包下创建一个AboutFinally类作为演示类,如图:
AboutFinally类代码如下:
packageknowledge.exception.complement; importjava.io.BufferedReader; importjava.io.FileReader; importjava.io.IOException; publicclassAboutFinally { publicstaticvoidmain(String[] args) { try { readText(""); } catch (IOExceptionioE) { ioE.printStackTrace(); } finally { System.out.println("释放资源!"); } } //定义一个方法用来读取传入的文本文件中的内容。publicstaticvoidreadText(StringfileName) throwsIOException { //直接抛出父类异常//进行判断,如果传入的路径不是文本文件,就提示它路径不合法if (!fileName.endsWith(".txt")) { thrownewIOException("你输入的路径不合法"); } //判断成功就开始读取文件//1.创建子符缓冲流对象,关联数据源文件BufferedReaderbufferedReader=newBufferedReader(newFileReader(fileName)); //定义变量,记录读取到的内容StringlineData; //循环读取,只要条件满足就一直读,并将读取到的内容赋值给变量while ((lineData=bufferedReader.readLine()) !=null) { System.out.println(lineData); } //释放资源bufferedReader.close(); } }
运行结果:
我们先传入错的路径,看看结果如何:
可以看到,即使JVM打印出了异常对象后,“释放资源” 打印在了后面.
接下里我们传入一个正确的路径试试,如下gif:
七、java 异常进阶:
java异常基础到这儿差不多就能上手了,java异常进阶准备与大家分享一下多异常的处理,自定义异常的使用等等内容,链接在这儿:
(正在写,写完放链接!)
11.26已更新:java 异常进阶 多异常和自定义异常详解。
八、完结撒❀:
恭喜你,看到这你肯定已经可以上手java异常类了!up还有其他关于javaSE基础的万字详解系列,有兴趣可以去我的博客看看,感谢阅读!
System.out.println("-----------------------------------------------------------------------------------------");