Java 异常处理 Java 基础语法 异常处理
吞吞吐吐大魔王 人气:0前些章节的知识点有时会涉及到异常的知识,如果没有专门学习过异常的小伙伴可能看的有点疑惑。今天这节就是为了讲解异常,让我们来了解什么是异常,它的作用是啥,怎么使用异常。
1. 异常的背景
1.1 邂逅异常
大家在学习 Java
时,应该也遇见过一些异常了,例如
算术异常:
System.out.println(10 / 0);
结果为:Exception in thread "main" java.lang.ArithmeticException: / by zero
数组越界异常:
int[] arr = {1, 2, 3, 4, 5}; System.out.println(arr[100]);
结果为:Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100
空指针异常:
int[] arr = null; System.out.println(arr.length);
结果为:Exception in thread "main" java.lang.NullPointerException
那么什么是异常呢?
异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。
1.2 异常和错误
- 异常被分为下面两种
运行时异常(非受查异常):
在程序运行(通过编译已经得到了字节码文件,再由 JVM 执行)的过程当中发生的异常,是可能被大家避免的异常,这些异常在编译时可以被忽略。
例如:算数异常、空指针异常、数组越界异常等等
编译时异常(受查异常):
编译时发生的异常,这个异常是大家难以预见的,这些异常在编译时不能被简单的忽略。
例如:要打开一个不存在的文件时,一个异常就发生了
除了异常我们我们也要了解下错误
错误:
错误不是异常,而是脱离程序员控制的问题,错误在代码中通常被忽略。
例如:当栈溢出时,一个错误就发生了,这是编译检查不到的
public static void func(){ func(); } public static void main(String[] args){ func(); }结果为:
Exception in thread "main" java.lang.StackOverflowError
那么异常和错误的区别是什么呢?
出现错误必须由我们程序员去处理它的逻辑错误,而出现异常我们只要去处理异常就好了
如果有疑惑的伙伴通过后面的介绍你会逐渐了解它们的区别
1.3 Java 异常的体系(含体系图)
Java
中异常的种类是很多的,我将一些异常收集并归类如下
其中
Error
是错误,Exception
是异常。而异常中又分为了两种,黄色的是编译时异常,橙色的是运行时异常。
但是这张图不仅仅说明了上述的关系,我们还要知道
每个异常其实都是一个类,并且箭头代表了继承的关系
我们可以通过一个代码来理解
int[] arr = null; System.out.println(arr.length); // 结果为:Exception in thread "main" java.lang.NullPointerException
此时我们点击这个异常 NullPointerException
就转到了它的定义,我们会看到
我们可以得到以下结论:
NullPointerException
是一个类- 这个类继承了
RuntimeException
这个类
为了刨根究底,我们继续转到 RuntimeException
这个类看看
我们又得到了以下结论:
RuntimeException
是一个类- 这个类继承了
Exception
这个类
继续刨根究底,我们又可以看到
诶,此时我们再对照着体系图我们就可以理解清除这张图的所有意思,并且此时对异常又有了个全面对认识
而今天我们的主角是异常,即 Exception
,接下来我将会对它进行解析。
1.4 异常的核心思想
作为一个程序员,我们经常都面对着
错误在代码中的存在我们不言而喻,因此就产生了两种主要针对错误的方式
方式一(LBYL):在操作之前就做充分的检查
方式二(EAFP):直接操作,有错误再解决
而异常的核心思想就是 EAFP
1.5 异常的好处
那么核心思想为 EAFP
的异常有什么好处呢?
我们可以随便举一个例子,比如你打一把王者,我们要进行登录、匹配、确认游戏、选择英雄等等的操作。
如果使用 LBYL
风格的代码,我们就要对每一步都做好充分的检查之后,再进行下一步,简单写个代码如下
boolean ret = false; ret = log(); if(!=ret){ // 处理登录游戏错误 return; } ret = matching(); if(!=ret){ // 处理匹配游戏错误 return; } ret = confirm(); if(!=ret){ // 处理确认游戏错误 return; } ret = choose(); if(!=ret){ // 处理选择游戏错误 return; }
而使用 EAFP
的风格,代码则是这样的
try{ log(); matching(); confirm(); choose(); }catch(登录游戏异常){ // 处理登录游戏错误 }catch(匹配游戏异常){ // 处理匹配游戏错误 }catch(确认游戏异常){ // 处理确认游戏错误 }catch(选择游戏异常){ // 处理选择游戏错误 }
两种方式的代码一对比,大家也可以看得出哪一种更好。EAFP
风格的就可以将流程和处理异常的代码分开,看起来更加舒服。而这也就是使用异常的好处之一。上述代码运用了异常的基本用法,后续会介绍。
2. 异常的基本用法
2.1 捕获异常
2.1.1 基本语法
try{ // 有可能出现异常的语句 }[catch(异常类型 异常对象){ // 出现异常后的处理行为 }...] [finally{ // 异常的出口 }]
try
代码块中放的是可能出现异常的代码catch
代码块中放的是出现异常后的处理行为finally
代码块中的代码用于处理善后工作,会在最后执行- 其中
catch
和finally
都可以根据情况选择加或者不加
2.1.2 示例一
首先我们看一个不处理异常的代码
int[] arr = {1, 2, 3}; System.out.println("before"); System.out.println(arr[100]); System.out.println("after");
结果是:
我们分析一下这个结果,首先它告诉我们在
main
方法中出现了数组越界的异常,原因就是100这个数字。下面它又告诉我们了这个异常的具体位置。并且通过这个结果我们知道,当代码出现异常之后,程序就中止了,异常代码后面的代码就不会执行了。
那么为什么这里抛出异常之后,后面的代码就不再执行了呢?
因为当没有处理异常的时候,一旦程序发生异常,这个异常就会交给 JVM 来处理。
而一旦交给了 JVM 处理异常,程序就会立即终止执行!
这也就是为什么我们会有自己处理异常这个行为
我们如果加上 try catch
自己处理异常
int[] arr = {1, 2, 3}; try { System.out.println("before"); System.out.println(arr[100]); System.out.println("after"); }catch(ArrayIndexOutOfBoundsException e){ System.out.println("数组越界!"); } System.out.println("after try catch");
结果是:
我们发现 try
中出现了异常的语句,并且我们针对这个异常做出了处理的行为。而 try catch
后面的程序依然可以继续执行
我们在上述代码中处理异常时 catch
里面用的语句就是直接告诉它出现了什么问题,但是如果我们想要知道这是什么异常,在代码的第几行有问题的话,就可以再加一个调用栈。
什么是调用栈呢?
方法之间存在相互调用关系,这种调用关系可以用“调用栈”来描述。在 JVM 中有一块内存空间称为“虚拟机栈”,这是专门存储方法之间调用关系的。当代码中出现异常的时候,我们就可使用 e.printStackTrace();
来查看出现异常代码的调用栈
2.1.3 示例二(含使用调用栈)
int[] arr = {1, 2, 3}; try { System.out.println("before"); System.out.println(arr[100]); System.out.println("after"); }catch(ArrayIndexOutOfBoundsException e){ System.out.println("数组越界!"); e.printStackTrace(); } System.out.println("after try catch");
结果是:
2.1.4 示例三(可以使用多个 catch 捕获不同的异常)
int[] arr = {1, 2, 3}; try { System.out.println("before"); System.out.println(arr[100]); System.out.println("after"); }catch(ArrayIndexOutOfBoundsException e){ System.out.println("数组越界!"); e.printStackTrace(); }catch(NullPointerException e){ System.out.println("空指针异常"); e.printStackTrace(); } System.out.println("after try catch");
这个代码里面有多个 catch
,他会捕获到第一个出现异常的位置
2.1.5 示例四(可以使用一个 catch 捕获所有异常,不推荐)
int[] arr = {1, 2, 3}; try { System.out.println("before"); System.out.println(arr[100]); System.out.println("after"); }catch(Exception e){ e.printStackTrace(); } System.out.println("after try catch");
其中我们使用了
Exception
这个类,我们知道它是所有异常的父类,因此可以用来捕获所有异常。但是这个方法是不推荐的,因为异常太多了,我们不容易定位问题
并且我们能得到一个结论
catch
进行类型匹配的时候,不光会匹配相同类型的异常,也能捕获目标异常类型的子类对象
2.1.6 示例五(使用 finally,它之间的代码将在 try 语句后执行)
int[] arr = {1, 2, 3}; try { arr = null; System.out.println(arr.length); }catch(NullPointerException e){ e.printStackTrace(); }finally{ System.out.println("finally 执行啦!"); } System.out.println("after try catch");
结果为:
我们紧接着再看一个代码,我将异常给改正确
int[] arr = {1, 2, 3}; try { System.out.println(arr.length); }catch(NullPointerException e){ e.printStackTrace(); }finally{ System.out.println("finally 执行啦!"); } System.out.println("after try catch");
上述代码就没有错误了,但是结果是
我们就得出了这个结论
无论
catch
是否捕获到异常,都要执行finally
语句
finally
是用来处理善后工作的,例如释放资源是可以被做到的。如果大家对于使用 finally
释放资源有疑惑,可以先看示例八,因为在 finally
中加入 Scanner
的 close
方法就是释放资源的一种例子
2.1.7 示例六(finally 引申的思考题)
public static int func(){ try{ return 10; }catch(NullPointerException e){ e.printStackTrace(); }finally{ return 1; } } public static void main(String[] args) { int num = func(); System.out.println(num); }
结果为:1
因为
finally
块永远是最后执行的。并且你也无法在这个代码之后执行其他语句,因为不管有没有捕获到异常都要执行finally
中的return
语句去终止代码
2.1.8 示例七(使用 try 负责回收资源)
在演示代码前要先补充一个关于 Scanner
的知识
我们知道使用
Scanner
类可以帮助我们进行控制台输入语句,但是Scanner
还是一种资源,而资源使用完之后是需要回收的,就像是我们打开了一瓶水喝了点还要盖上它。故用完后我们可以加上close
方法来进行回收,如Scanner reader = new Scanner(System.in); int a = reader.nextInt(); reader.close();
而 try 有一种写法可以在它执行完毕后自动调用 Scanner
的 close
方法
try(Scanner sc = new Scanner(System.in){ int num = sc.nextInt(); }catch(InputMismatchException e){ e.printStackTrace(); }
而这种方式的代码风格要比使用 finally 中含有 close 方法要好些
2.1.9 示例八(本方法中没有合适的处理异常方式,就会沿着调用栈向上传递)
public static void func(){ int[] arr = {1, 2, 3}; System.out.println(arr[100]); } public static void main(String[] args){ try{ func(); }catch(ArrayIndexOutOfBoundsException e){ e.printStackTrace(); } }
结果为:
由于我们写 func
方法时出现了异常没有及时处理,但我们在 main 方法中调用它了,所以就经过方法之间互相的调用关系,我们一直到了 main
方法被调用的位置,并且此时有合适的处理异常的方法
若最终没有找到合适的异常处理方法,最终该异常就会交给 JVM 处理,即程序就会终止
2.1.10 异常处理流程总结
- 程序先执行
try
中的代码 - 如果 try 中的代码出现异常,就会结束 try 中异常之后的代码,并查看该异常和
catch
中的异常类型是否匹配 - 如果匹配,就会执行
catch
中的代码 - 如果没有匹配的,就会将异常向上传递到上层调用者
- 无论是否找到匹配类型,
finally
中的代码都会被执行 - 如果上层调用者没有处理异常的方法,就会继续向上传递
- 一直到 main 方法也没有合适的代码处理异常,就会交给
JVM
来处理,此时程序就会终止
2.2 抛出异常
以上我们介绍的都是 Java
内置的类抛出的一些异常,除此之外我们也可以使用关键字 throw 手动抛出一个异常,如
public static int divide(int x, int y) { if (y == 0) { throw new ArithmeticException("抛出除 0 异常"); } } public static void main(String[] args) { System.out.println(divide(10, 0)); }
该代码就是我们手动抛出的异常,并且手动抛出的异常还可以使用自定义的异常,后面将会介绍到
2.3 异常说明
我们在处理异常时,如果有一个方法,里面很长一大段,我们其实是希望很简单的就知道这段代码有可能会出现哪些异常。故我们可以使用关键字 throws
,把可能抛出的异常显示的标注在方法定义的位置,从而提醒使用者要注意捕获这些异常,如
public static int divide(int x, int y) throws ArithmeticException{ if (y == 0) { throw new ArithmeticException("抛出除 0 异常"); } }
注意:
如果我们将
main
方法抛出一个异常说明,而main
方法的调用者是JVM
,所以如果在mai
n 函数上抛出异常的话,就相当于JVM
来处理这个异常了
3. 自定义异常类
Java
中虽然有丰富的异常类,但是实际上肯定还要一些情况需要我们对这些异常进行扩展,创建新的符合情景的异常。
那怎么创建自定义异常呢?首先我们就可以去看看原有的那些异常是怎么做的
两异常
我们发现这两个异常都是继承在
RuntimeException
这个类的,并且都构造了两个构造方法,分别是不带参数和带参数
而我模拟了一个登录账号的代码
public class TestDemo { private static String userName = "root"; private static String password = "123456"; public static void main(String[] args) { login("admin", "123456"); } public static void login(String userName, String password) { if (!TestDemo.userName.equals(userName)) { // 处理用户名错误 } if (!TestDemo.password.equals(password)) { // 处理密码错误 } System.out.println("登陆成功"); } }
通过这个模拟的场景,我们可以针对运行时账号和密码是否正确写一个异常
class UserException extends RuntimeException{ public UserException(){ super(); } public UserException(String s){ super(s); } } class PasswordException extends RuntimeException{ public PasswordException(){ super(); } public PasswordException(String s){ super(s); } }
紧接着我们再手动抛出异常
public class TestDemo { private static String userName = "root"; private static String password = "123456"; public static void main(String[] args) { login("admin", "123456"); } public static void login(String userName, String password) { if (!TestDemo.userName.equals(userName)) { throws new UserException("用户名错误"); } if (!TestDemo.password.equals(password)) { throws new PasswordException("密码错误"); } System.out.println("登陆成功"); } }
所以我们创建新的异常时,就是先思考这是哪种类型的异常,再照猫画虎。但是可能有疑惑,如果我们新建异常时统一继承 Exception
不就行吗?
No!由于
Exception
分为编译时异常和运行时异常,使用Exception
的话默认是编译时异常(即受查异常),而一段代码可能抛出受查异常则必须显示进行处理。
故如果我们将上述新建的异常继承 Exception
的话,就要再对代码中的异常进行处理,否则会直接报错
加载全部内容