前言
每当我们编写Java程序时,都会遇到各种各样的错误。有时候,错误可能是因为程序逻辑本身的问题,比如除以0的情况。有时候,错误可能是因为输入的数据有误,比如输入了一个无法转换为整数的字符串。无论哪种情况,Java都提供了一种机制来处理这些错误——Java异常
。
这些非正常情况在Java中统一被认为是异常,Java使用异常机制来统一处理
在本文中,我们将深入了解Java异常的基本概念,并学习如何来捕获和处理异常。
一、初识异常
其实在学习异常之前,我们在平时的编码中就已经见过了异常,例如:
(1)使用null访问时产生的空指针异常:java.lang.NullPointerException
(2)数组越界引发的索引越界异常:java.lang.ArrayIndexOutOfBoundsException
(3)除0引发的算术异常:java.lang.ArithmeticException
上面的几种异常都称为非受查异常,并且是在ava虚拟机在程序执行期间根据特定条件自动抛出的异常。
其实对于Java中的异常体系来说,这几种异常只不过是冰山一角,我们继续往下看:
二、异常的体系结构
当我们的程序遇到问题时,就会抛出异常,观察窗口弹出的异常我们很容易发现,其实异常本质上就是java.long包下面的一个类。
Java异常体系结构是由一系列的异常类组成的。异常种类繁多,为了对不同异常或者错误进行很好的分类管理,Java内部维护了一个异常的体系结构。在Java中,以java.lang.Throwable为异常体系的顶层类,派生出一系列的子类,部分类如下图:
Throwable:是异常体系的顶层类,其派生出两个重要的子类, Error 和 Exception
Error:指的是Java虚拟机无法解决的严重问题,比如:JVM的内部错误、资源耗尽等,典型代表:栈溢出错误StackOverflowError、内存溢出错误OutOfMemoryErro
Exception:异常产生后程序员可以通过代码进行处理,使程序继续执行。比如:感冒、发烧。我们平时所说的异常就是Exception。
三、异常的分类
在Java中我们通常将异常分为(Checked Exception)受查异常和(Unchecked Exception)非受查异常,或是编译时异常和运行时异常。
受查异常(编译时异常)
受查异常必须在程序中处理或声明,否则程序将无法编译。
非受查异常(运行时异常)
非受查异常可以在程序中处理和声明,但不是必需的。
四、异常的处理
(1)throw-异常抛出
Java中主要有两种触发异常的方式:
1.代码自己执行的过程当中触发异常。如上面的”认识异常“中举出的例子。
2.使用
throw
关键字手动抛出异常。如throw new NullPointerException();
所有异常类都有一个共同的父类Throwable
,它有4个public构造方法:
public Throwable() public Throwable(String message) public Throwable(String message, Throwable cause) public Throwable(Throwable cause)
在抛出异常的时候我们可以合理的使用构造方法,例如输入错误提示信息:
注意事项:
throw
必须写在方法体内部- 抛出的对象必须是
Exception
或者 Exception 的子类对象- 如果抛出的是
RunTimeException
或者RunTimeException
的子类,则可以不用处理,直接交给JVM来处理- 如果抛出的是编译时异常,用户必须处理,否则无法通过编译
- 异常一旦抛出,其后的代码就不会执行
拓展: throw关键字可以与return关键字进行对比,return代表正常退出,throw代表异常退出,return的返回位置是确定的,就是上一级调用者,而throw后执行哪行代码则经常是不确定的,由异常处理机制动态确定。
(2)throws-异常声明
public void test() throws NullPointerException, CloneNotSupportedException, ArrayIndexOutOfBoundsException { //.... }
throws用于声明一个方法可能抛出的异常,跟在方法的括号后面,可以声明多个异常,以逗号分隔。这种声明的含义是说,我这个方法内可能抛出这些异常,我没有进行处理,至少没有处理完,提醒方法的调用者处理异常。
如果一个方法内调用了另一个声明抛出受查异常(checked)的方法,则必须处理这些受查异常(checked)进行处理,此时可以使用try-catch对异常进行捕获处理,如果仍然没有能力进行处理,则可以继续使用throws声明可能抛出的异常,如下代码所示:
public void test() throws CloneNotSupportedException { super.clone(); } // 方式1:继续抛出 public void tester1() throws CloneNotSupportedException { test(); } // 方式2:处理 public void tester2() { try { test(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } }
(try-catch下面具体介绍)
注意事项:
throws
必须跟在方法的参数列表之后。- 声明的异常必须是
Exception
或者 Exception 的子类。- 方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可。
- 对于非受查异常(unchecked),是不要求使用throws进行声明的,但对于受查异常(checked),则必须进行声明,换句话说,如果没有声明,则不能抛出。
- 对于受查异常,不可以抛出而不声明,但可以声明抛出但实际不抛出。
(3)try-catch-捕获处理
我们上面说到,如果此方法内不想处理异常,或没有能力处理异常,我们可以使用throws声明可能发生的异常。可见throws对异常并没有真正处理,而是将异常报告给抛出异常方法的调用者,由调用者处理。如果真正要对异常进行处理,就需要try-catch。
try-catch语法:
(1)基本规则:异常处理机制将根据抛出的异常类型找第一个匹配的catch
块,找到后,执行catch块内的代码,其他catch块就不执行了,如果没有找到,会继续到上层方法中查找。
try { // 可能会抛出异常的代码 } catch (SomeException1 ex) { // 处理SomeException1异常的代码 } catch (SomeException2 ex) { // 处理SomeException2异常的代码 } catch (SomeException3 ex) { // 处理SomeException3异常的代码 }[ catch…… ]//根据需求增减catch // 一旦异常被捕获处理了,try-catch后的代码会执行 //继续执行代码...
(2)在捕获多种异常时,如果多个异常的处理方式是完全相同, 也可以写成这样:
catch (SomeException1 | SomeException2 e) { //处理异常... }
(3)如果异常之间具有父子关系,语法规定:一定是子类异常在前catch,父类异常在后catch
try { //可能会抛出异常的代码块 } catch (ArrayIndexOutOfBoundsException e) { //处理异常 } catch (Exception e){ //处理异常 }
(4)可以通过一个catch捕获所有的异常,即多个异常一次捕获,但是使用这种方法捕获的异常没有提供足够的信息,无法对异常进行有效的处理,可能会对程序造成严重的后果。不推荐使用!
try { // 可能会抛出多种类型的异常 } catch (Exception ex) { // 处理所有类型的异常的代码 }
例如使用try-catch
捕获处理数组下标越界异常:
public static void test() { int[] array1={1,2,3}; System.out.println(array1[10]); } public static void main(String[] args){ try { test(); System.out.println("异常产生后这里的代码不在执行"); } catch (ArrayIndexOutOfBoundsException e) { e.printStackTrace(); //打印异常信息 System.out.println("捕获到异常:ArrayIndexOutOfBoundsException.处理异常……"); } System.out.println("这是一行正常代码"); }
异常发生后通常会产生异常信息,上面的例子中我们对异常信息进行输出,在处理异常时可以的对异常信息加以利用:
(1)e.getMessage()获取异常信息
(2)System.out.println(e)打印异常类型+异常信息
(3)e.printStackTrace()打印异常栈到标准错误输出流。(使用最多)
作用:通过这些信息有助于理解为什么会出异常,还可以帮我们快速定位异常发生的位置,这是解决编程错误的常用方法。示例是直接将信息输出到标准流上,实际系统中更常用的做法是输出到专门的日志中。
(4)finally
try-catch异常机制中还有一个重要的部分,就是finally
。catch后面可以跟finally语句,语法如下所示:
try{ //可能抛出异常 }catch(Exception e){ //捕获异常 }finally{ //不管有无异常都执行 }
对于finally内的代码不管有无异常发生,都会执行。具体来说:
- 如果没有异常发生,在try内的代码执行结束后执行。
- 如果有异常发生且被catch捕获,在catch内的代码执行结束后执行
- 如果有异常发生但没被捕获,则在异常被抛给上层之前执行。
finally作用: 有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库连接、IO流等,在程序正常或者异常退出时,必须要对资源进进行回收避免造成资源泄漏。另外,因为异常会引发程序的跳转,可能导致有些语句执行不到(如在try中return),finally就是用来解决这个问题的。
例如在读取文件内容时可以这样处理:
public static void fileTest() { FileReader reader = null; try { reader = new FileReader("somefile.txt"); // 使用reader读取文件内容 } catch (FileNotFoundException ex) { System.out.println("未找到文件:somefile.txt"); } catch (IOException ex) { // 处理IOException异常的代码... } finally { if (reader != null) { try { //关闭FileReader对象 reader.close(); } catch (IOException ex) { // 处理关闭reader时抛出的IOException异常的代码 } } } }
(5)try-catch-finally与return
代码一:
public static int test(){ int num = 0; try{ return num; }finally{ num = 10; } }
test返回值为0
.实际执行过程是,在执行到try内的return ret;语句前,会先将返回值ret保存在一个临时变量中,然后才执行finally语句,最后try再返回那个临时变量,finally中对ret的修改不会被返回。
代码二:
public static int func() { try { return 10; } finally { return 20; } }
func返回值为20. try 或者 catch 中如果有 return 会在这个 return 之前执行 finally(代码一特殊)。但是如果finally 中也存在 return 语句, 那么就会执行 finally 中的 return, 从而不会执行到 try 中原有的 return。
小结:一般而言,为避免混淆,应该避免在finally中使用return语句或者抛出异常,如果调用的其他代码可能抛出异常,则应该捕获异常并进行处理。
五、防御式编程
为了防止程序出现错误和异常,我们引出了防御式编程的思想,根据处理异常的时机不同,将其分为以下两种模式:
1.LBYL: Look Before You Leap.
事前防御型
boolean ret = false; ret = 登陆游戏(); if (!ret) { 处理登陆游戏错误; return; } ret = 开始匹配(); if (!ret) { 处理匹配错误; return; } ret = 游戏确认(); if (!ret) { 处理游戏确认错误; return; }
缺陷:正常流程和错误处理流程代码混在一起, 代码整体显的比较混乱。
2.EAFP: It's Easier to Ask Forgiveness than Permission.
事后认错型
try { 登陆游戏(); 开始匹配(); 游戏确认(); } catch (登陆游戏异常) { 处理登陆游戏异常; } catch (开始匹配异常) { 处理开始匹配异常; } catch (游戏确认异常) { 处理游戏确认异常; } }
优势:正常流程和错误流程是分离开的, 程序员更关注正常流程,代码更清晰,容易理解代码。异常处理的核心思想就是 EAFP。
六、异常的处理流程
总体流程:异常处理机制会从当前函数开始查找看谁"捕获"了这个异常,当前函数没有就查看上一层,直到主函数,如果主函数也没有,就使用默认机制,把这个异常交给JVM处理,即输出异常栈信息并退出。
具体来说:
- 程序先执行
try
中的代码 - 如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和
catch
中的异常类型是否匹配. - 如果找到匹配的异常类型, 就会执行
catch
中的代码 - 如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者.
- 无论是否找到匹配的异常类型,
finally
中的代码都会被执行到(在该方法结束之前执行). - 如果上层调用者也没有处理的了异常, 就继续向上传递.
- 一直到
main
方法也没有合适的代码处理异常, 就会交给JVM
来进行处理, 此时程序就会异常终止.
七、自定义异常类
Java 中虽然已经内置了丰富的异常类, 但是并不能完全表示实际开发中所遇到的一些异常,此时就需要维护符合我们实际情况的异常结构——自定义异常类
注意事项:
- 自定义异常通常会继承自 Exception 或者 RuntimeException
- 继承自 Exception 的异常默认是受查异常 继承自
- RuntimeException 的异常默认是非受查异常
下面我们实现一个登录功能,并加入自定义的登录异常:
//自定义异常类 class InvalidUsernameException extends Exception { //帮助构造父类构造方法 public InvalidUsernameException(String message) { super(message); } } class InvalidPasswordException extends Exception { //帮助构造父类构造方法 public InvalidPasswordException(String message) { super(message); } }
public class Login { // 用于存储用户名和密码 private String userName = "bumoyu"; private String password = "123456"; public void login(String username, String password) throws InvalidUsernameException,InvalidPasswordException { if (!this.userName.equals(username)) { // 如果用户名不正确,抛出异常 throw new InvalidUsernameException("密码无效!"); } if (!this.password.equals(password)) { // 如果密码不正确,抛出异常 throw new InvalidPasswordException("用户名无效!"); } } //测试登录: public static void main(String[] args) { Login test = new Login(); try { test.login("zhangsan","123456"); System.out.println("登录成功!"); } catch (InvalidPasswordException e) { e.printStackTrace(); } catch (InvalidUsernameException e) { e.printStackTrace(); } } }
小结
总之,Java异常是Java程序中一种重要的错误处理机制。通过使用异常,我们可以更方便地处理程序中出现的错误,并维护程序的正确性和可靠性。