每当我们编写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 和 ExceptionError
:指的是Java虚拟机无法解决的严重问题,比如:JVM的内部错误、资源耗尽等,典型代表:栈溢出错误StackOverflowError、内存溢出错误OutOfMemoryErroException
:异常产生后程序员可以通过代码进行处理,使程序继续执行。比如:感冒、发烧。我们平时所说的异常就是Exception。
在Java中我们通常将异常分为(Checked Exception)受查异常和(Unchecked Exception)非受查异常,或是编译时异常和运行时异常。
受查异常(编译时异常)
受查异常必须在程序中处理或声明,否则程序将无法编译。
非受查异常(运行时异常)
非受查异常可以在程序中处理和声明,但不是必需的。
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后执行哪行代码则经常是不确定的,由异常处理机制动态确定。
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),则必须进行声明,换句话说,如果没有声明,则不能抛出。
- 对于受查异常,不可以抛出而不声明,但可以声明抛出但实际不抛出。
我们上面说到,如果此方法内不想处理异常,或没有能力处理异常,我们可以使用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()
打印异常栈到标准错误输出流。(使用最多)
作用:通过这些信息有助于理解为什么会出异常,还可以帮我们快速定位异常发生的位置,这是解决编程错误的常用方法。示例是直接将信息输出到标准流上,实际系统中更常用的做法是输出到专门的日志中。
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异常的代码
}
}
}
}
代码一:
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程序中一种重要的错误处理机制。通过使用异常,我们可以更方便地处理程序中出现的错误,并维护程序的正确性和可靠性。