一、什么是异常
异常就是程序运行中的不正常行为(而非编译过程中,所以能通过编译),它不同于 bug (是不允许出现的问题),它是为了增加代码的鲁棒性,比如处理用户的网络不通畅、输入数据格式不对、内存不够等问题(是允许出现的问题)。
出现异常的一些示例:
注意,100 / 0.0 不是算术异常,在高等数学中等于无穷大:
二、异常的体系结构
除了上面示例中的算术异常、数组越界异常、空指针异常,还有许多其它类别的异常,由 Java 标准库提供,且形成了一个完整的异常的体系结构。而其它语言,例如 C++,虽然也有异常的概念,但属于半成品,不好用,所以不推荐使用。Java 开发当中,异常是必不可少的。
异常的体系结构分为两大部分:Error,由 JVM 内部使用;Exception,由程序员使用,就是我们通常说的异常。
三、异常的分类
Exception 分为两类:受查异常和非受查异常。写代码时,受查异常必须要显示处理,否则编译不通过;非受查异常则不需要显示处理。
打一个比方:感冒、蚊虫叮咬、黑眼圈之类的小毛病,可以自愈,且无大碍,就无需就医(受查异常);急性胃炎、食物中毒之类的大问题,就必须就医或者动手术(非受查异常)。
受查异常示例:
未处理异常,编译不通过。按住 alt+shift+enter,自动添加抛出异常:
RunTimeException 类及其子类异常,都属于非受查异常,即(一)中的示例。
受查异常也叫编译时异常,非受查异常也叫运行时异常,但个人不喜欢这种叫法。异常都是在程序运行时发生的,这种叫法很容易引起误会。
四、throw 和 throws
throw 表示抛出一个异常对象,是真的抛出;而 throws 表示声明该方法抛出了一些异常对象,属于画饼,实际抛没抛出是不知道的,目的是让调用者知道调用该方法需要处理异常。
1、throw
- 必须写在方法体内部。
- 抛出的必须是 Exception 或 Exception 子类的对象。
- 若抛出的是非受查异常对象,则不用处理,交给 JVM 处理。
- 若抛出的是受查异常对象,则必须处理(声明抛出异常对象),否则编译不通过。
- 异常一旦抛出,其后的代码就不执行。
2、throws
- 必须写在方法参数列表后。
- 声明的必须是 Exception 或 Exception 子类。
- 一个方法同时抛出多个异常时,用逗号隔开。若这多个异常存在父子关系,声明父类异常即可。
但是,也不能通通直接声明抛出所有异常类的父类 Exception,因为这样仅仅知道可能会出现异常,但不知道异常的准确类型,难以针对性处理。就好像知道对象生气了,但是对象不告诉你生气的原因。
五、异常的处理
异常是客观存在的,当出现异常,需要进行处理,处理的方式有两种:LBYL 和 EAFP。
1、事前防御型(LBYL)
"Look Before Your Leap.",在操作之前就做检查。即先问后做。
执行一个操作前,先检查其异常并处理。
boolean ret = false;
ret = 登陆游戏();
if (!ret) {
处理登陆游戏错误;
return;
}
ret = 开始匹配();
if (!ret) {
处理匹配错误;
return;
}
ret = 游戏确认();
if (!ret) {
处理游戏确认错误;
return;
}
ret = 选择英雄();
if (!ret) {
处理选择英雄错误;
return;
缺点:正常和异常流程混在一起,代码不易理解。
这种处理方式,在 C、C++ 等这种不支持异常或者处理异常能力比较弱的编程语言中使用。
2、事后认错型(EAFP)
"It's Easier to Ask Forgiveness than Permission.",事后获取原谅比事前得到许可更简单。即先做后问。
先执行操作,如果操作抛出异常,再到对应的 catch 进行处理。
try {
登陆游戏();
开始匹配();
游戏确认();
选择英雄();
...
} catch (登陆游戏异常) {
处理登陆游戏异常;
} catch (开始匹配异常) {
处理开始匹配异常;
} catch (游戏确认异常) {
处理游戏确认异常;
} catch (选择英雄异常) {
处理选择英雄异常;
}
...
优势:正常和异常流程分开,我们更关注的正常流程逻辑清晰,代码更易理解。
这种处理方式,在 Java 这种处理异常能力强的编程语言中使用。
六、异常的捕获和处理 try-catch
调用方法时,使用 try-catch 捕获和处理该方法声明抛出的异常。
1、try 内抛出异常后,后续代码的执行
若抛出异常处,被 catch 捕获,则 try 内后续代码不会执行,try-catch 外后续代码会执行:
printStackTrace 可以打印调用栈(层层抛出异常的位置)。
若抛出异常处,没有匹配的 catch,即没被捕获,则会向上一级调用者抛出,到最后 main 方法的调用者是 JVM,若 main 没捕获异常,则最后被 JVM 处理,造成程序崩溃,退出码为非 0。换句话说,根据前面的结论,一旦抛出异常,后面的程序都不会执行:
2、catch 多个异常
当多个异常的处理方式相同,可以写成:
父类异常可以捕获子类异常:
当多个异常,存在父子关系时,必须子类在前,父类在后(如果父类在子类前,父类把所所有子类及父类本身都捕获了,后面再跟个捕获子类就没意义了):
七、finally
我们知道,遇到 return 或者异常抛出,后续的代码都不会执行。当我们想 return 或者异常抛出后,立马释放资源怎么办?可以使用 finally(在当前方法结束前执行):
实际的执行顺序:try 内抛出异常 >> finally 内释放资源 >> 层层抛出程序崩溃。但打印的结果是 “释放资源” 在最后,这里面有原因,println 和 printStackTrace 打印方式不同,具体原因可能以后会学到。
如果 try、catch、finally 块都有 return,则 finally 会覆盖掉前面的:
八、自定义异常类
异常处理,是根据业务来写的。比如电商平台,可能存在商品库存不足、用户余额不足等异常等。此时 Java 标准库提供的异常类可能不够我们使用,我们就需要根据业务自定义异常类。
自定义异常类继承 Exception 类或者 RuntimeException 类。继承 Exception 类默认为受查异常;继承 RuntimeException 类默认为非受查异常。
示例,实现账号登陆异常处理:
// 用户名异常类
class UserNameException extends Exception {
public UserNameException(String message) {
super(message);
}
}
// 密码异常类
class PasswordException extends Exception {
public PasswordException(String message) {
super(message);
}
}
// 登录
class Login {
public void login(String username, String password) throws UserNameException, PasswordException {
if (!username.equals("admin"))
throw new UserNameException("Invalid username"); // 字符串构造方法,出现异常的原因
if (!password.equals("123"))
throw new PasswordException("Invalid password");
System.out.println("Login successful");
}
}
public class Test2 {
public static void main(String[] args) {
Login login = new Login();
try {
login.login("张三", "123456");
} catch (UserNameException | PasswordException e) { // 捕获异常
System.out.println(e.getMessage());
}
}
}