1.异常的概念以及体系结构
1.1 异常的概念
在生活中人会生病,比如咳嗽流鼻涕,头晕等,程序也一样,比如:数据格式不匹配,网络不通畅,内存报警等.在Java中,我们把程序执行的不正常行为称为异常.
比如:
1. 算数异常
System.out.println(10 / 0);
// 执行结果Exception in thread "main" java.lang.ArithmeticException: / by zero
2.数组越界异常
nt[] arr = {1, 2, 3}; System.out.println(arr[100]);
// 执行结果Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100
3.空指针异常
int[] arr = null; System.out.println(arr.length);
// 执行结果Exception in thread "main" java.lang.NullPointerException
从以上例子我们可以看出,java中不同类型的异常,都有对应的类来进行描述,简而言之,异常其实就是类
1.2 异常的体系结构
为了对异常进行一个比较好的分类,Java内部维护了一个异常的体系结构:
由图可知:
1. Throwable是所有异常的父类,然后由它派生出俩个子类: Error 和 Exception
2. Error: java虚拟机无法解决的严重错误, 比如: JVM的内部错误,资源耗尽(栈溢出:StackOverflowError和OutOfMemoryError)一旦发生,没有任何东西能阻止.如:人的死亡
3. Exception: 异常产生之后程序员可以通过代码处理,使程序继续执行,比如:普通的感冒,发烧.
1.3 异常的分类
1. 编译时异常
在编译时期发生的异常(写在编译器上就会冒红(非语法错误)),也成为受查异常(Checked Exception)
比如之前写的clone()方法
编译时出现的语法性错误,不能称之为异常。例如将 System.out.println 拼写错了, 写成了system.out.println. 此时编译过程中就会出错, 这是 "编译期" 出错。而运行时指的是程序已经编译通过得到class 文件了, 再由 JVM 执行过程中出现的错误
2. 运行时异常
在程序执行期间发生的异常,也成为非受查异常(Unchecked Exception)
比如算数异常
RunTimeException以及其子类对应的异常,都称为运行时异常。比如:NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException...
具体想知道运行谁是运行时异常的子类可以自行去java官方文档查看
2.异常的处理
2.1 防御式编程
错误在代码中是客观存在的. 因此我们要让程序出现问题的时候及时通知程序猿. 主要的方式
1. 事前防御型: LBYL: Look Before You Leap. 在操作之前就做充分的检查.
boolean ret = false;
ret = 登陆游戏();
if (!ret) {
处理登陆游戏错误;
return;
} ret = 开始匹配();
if (!ret) {
处理匹配错误;
return;
} ret = 游戏确认();
if (!ret) {
处理游戏确认错误;
return;
} ret = 选择英雄();
if (!ret) {
处理选择英雄错误;
return;
}
缺点: 代码比较混乱
2. 事后认错型: It's Easier to Ask Forgiveness than Permission. "事后获取原谅比事前获取许可更容易". 也就是先操作, 遇到问题再处理. (有点先斩后奏的味道了)
try {
登陆游戏();
开始匹配();
游戏确认();
选择英雄();
载入游戏画面();
...
} catch (登陆游戏异常) {
处理登陆游戏异常;
} catch (开始匹配异常) {
处理开始匹配异常;
} catch (游戏确认异常) {
处理游戏确认异常;
} catch (选择英雄异常) {
处理选择英雄异常;
} catch (载入游戏画面异常) {
处理载入游戏画面异常;
}
......
throw new XXXException("异常产生的原因");
public static int getElement(int[] array, int index){
if(null == array){
throw new NullPointerException("传递的数组为null");
优势: 正常流程和错误流程是分开的,代码更加清晰.
在Java中,异常处理主要用的关键字: throw,try,catch,final,throws. 接下来我们要具体介绍这几个关键字.
2.2 异常的抛出
抛出异常的方式有很多:
1. 手动触发
2. 某段程序触发
我们主要讲1, 手动触发异常,我们要使用一个关键字: throw , 抛出一个指定的异常对象,将错误信息告知给调用者(一般抛出的是自定义异常)
语法格式:
new throw xxxException("产生异常的原因")
比如这数组指向为空的异常.就是我们手动跑出来的
注:
1. throw必须写在方法体内部
2. 抛出的对象必须是Exception 或者 Exception 的子类对象
3. 如果抛出的是编译时异常,用户必须处理,不然用户无法通过编译.
4. 异常一旦抛出,后面的代码就不能执行了
2.3 异常的捕获
我们捕获异常一般有俩种方法: throws 和 try-catch捕获处理
1. 异常申明throws
当用户在面对一个编译时(受查异常)的时候选择不处理,就可以借助throws来把异常抛给方法的调用者来处理.告诉方法的调用者,调用这个方法,就会抛出一个XXX异常.即当前方法不处理异常,提醒方法的调用者处理异常.
语法格式:
修饰符 返回值类型 方法名字(参数列表) throws 异常类型1,异常类型2...{}
放在参数列表的后面
比如上面的clone().
我们再看看Object里面的clone()方法
因为所有的类都默认继承Object类,又因为Object里面抛出了CloneNotSupportedException,所以继承它的类重写它的clone方法都要throws这个异常(如果不处理).
注:
1. throws必须跟在方法的参数列表之后
2. 声明的异常必须是 Exception 或者 Exception 的子类
3. 方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型,具有父子关系,直接声明父类即可
public class Config {
File file;
// public void OpenConfig(String filename) throws IOException,FileNotFoundException{
// FileNotFoundException 继承自 IOException
public void OpenConfig(String filename) throws IOException{
if(filename.endsWith(".ini")){
throw new IOException("文件不是.ini文件");
} if(
filename.equals("config.ini")){
throw new FileNotFoundException("配置文件名字不对");
} // 打开文件
}
public void readConfig(){
}
}
2. 使用try-catch来进行捕获并且处理
上面的throws并没有对异常进行处理,如果真的要由调用者来处理的话就要使用try-catch.
语法格式
try{
此处是有可能出现异常的代码
}catch(要捕获的类型1 e){
如果try抛出的异常和catch捕获的异常类型一致,或者是try的父类,就会被捕获到,然后对异常进行正确处理,处理完之后,跳出try-catch,继续执行后续代码
}catch(要捕获的类型2 e){
同上
}finally{
这里的代码一定会执行到(一般用来关闭资源)
}
后续代码,当异常捕获到,异常被处理了,这里的代码一定会执行,但是捕获到了,但是捕获的类型不对,这里的代码就不会被执行.
注意: try中的代码可能会抛出异常,也可能不会
我们来看一个例子:
public static void main(String[] args) {
System.out.println("before......");
try {
System.out.println(10/0);
System.out.println("11234");//这里的代码不能打印
}catch (ArithmeticException e) {//这里面的参数就是你要捕获的异常(要匹配),捕获到了才会执行catch当中的内容
//这个玩意是使用其他工具来打印的,不是用System.out.println
e.printStackTrace();
System.out.println("我来处理ArithmeticException异常了");
}catch (Exception e) {//我们可以通过catch捕获多个异常,但是同一时刻只能抛出一个异常
System.out.println("Exception异常了");//这个相当于充当了殿后的角色
}
System.out.println("after......");//这个代码不try catch就是jvm来处理的
}
运行结果:
注意:
1. try里面异常代码后面的正常代码是不会被执行的
2. try里面捕获了才会执行catch续代码,catch里面的异常和你try里面捕获的异常要匹配(异常是按照类型来捕获的),如果抛出异常类型与catch时异常类型不匹配,即异常不会被成功捕获,也就不会被处理,就会层层往上抛直到main没处理就会交给JVM.JVM收到后中断程序,这个和异常不进行(throws,try-catch)处理的结果一样
这里有张我自己总结的图:
3. e.printStackTrace(),打印的是这一块信息
4. 一般我们习惯把父类异常写在子类异常之后当作兜底,比如ArithmeticExceptionn继承于Exception,我们就把后者写在最后一个catch里面.(如果反着写就会报错)
5. 由于 Exception 类是所有异常类的父类. 因此可以用这个类型表示捕捉所有异常
异常的种类有很多, 我们要根据不同的业务场景来决定.
对于比较严重的问题(例如和算钱相关的场景), 应该让程序直接崩溃, 防止造成更严重的后果对于不太严重的问题(大多数场景), 可以记录错误日志e.printStackTrace(), 并通过监控报警程序及时通知程序猿对于可能会恢复的问题(和网络相关的场景), 可以尝试进行重试.
在我们当前的代码中采取的是经过简化的第二种方式. 我们记录的错误日志是出现异常的方法调用信息, 能很快速的让我们找到出现异常的位置. 以后在实际工作中我们会采取更完备的方式来记录异常信息.
3. 关于finally
一般,程序员在写代码的时候,有些代码不管程序发布发生异常都要执行,比如:程序种打开的资源: 网络连接,数据库连接,IO流等,不管在申明情况下都要对资源进行回收. 另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的。
语法格式:
try{
// 可能会发生异常的代码
}catch(异常类型 e){
// 对捕获到的异常进行处理
}finally{
// 此处的语句无论是否发生异常,都会被执行到
} / / 如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行
比如文件操作:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TryCatchFinallyExample {
public static void main(String[] args) {
BufferedReader reader = null;
try {
// 打开文件并创建BufferedReader对象
reader = new BufferedReader(new FileReader("example.txt"));
// 逐行读取文件内容并打印
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
// 处理可能的IO异常
System.err.println("发生IO异常: " + e.getMessage());
} finally {
// 确保资源被关闭
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
System.err.println("关闭文件时发生异常: " + e.getMessage());
}
}
}
}
}
这里面: reader.close(); 就是在回收资源.
2.4 常考面试题
1. throw和throws的区别
throw是当你需要在代码中手动抛出一个异常时,可以使用 throw.手动抛出一个异常
throws是把这个异常往上层调用抛(不进行处理),用try-catch才进行处理.主要用于声明一个方法可能抛出的异常类型。(一般面向是受查异常)
2.finally中的语句一定会执行吗?
正常情况下:try-catch-finally,无论能否捕获/类型匹不匹配,都会执行finally
特殊情况: 如果在try或者catch里面调用了System.exit(0),程序就会立即终止,不会执行finally.再如:在try-catch里面代码进入了死循环或者阻塞Thread.sleep(很长时间),都不会执行finally.
再如虚拟机崩溃,JVM被强制关闭(操作系统,硬件外部干预,如直接kill杀死进程),finally也不执行.
2.5 异常的处理流程
关于调用栈
方法之间是存在相互调用关系的, 这种调用关系我们可以用 "调用栈" 来描述. 在 JVM 中有一块内存空间称为"虚拟机栈" 专门存储方法之间的调用关系.
这个东西之前我们就提到过:e.printStackTrace();可以看见异常代码的调用栈.如果本方法中没有合适的处理异常的方式, 就会沿着调用栈向上传递,如果向上一直传递都没有合适的方法处理异常, 最终就会交给 JVM 处理, 程序就会异常终止(和我们最开始未使用 try catch 时是一样的).层层向上直到mian为止还没有处理就给jvm直接终止这个程序.
异常的处理流程小总结:
1. 程序先执行try里面的代码
2. 如果try种的代码出现异常就会结束try的代码,看和catch里面的异常代码是否匹配
3. 如果找到匹配的异常类型,就会执行catch中的代码(否则就把异常传递给上层调用者,直到处理(正常运行)或者传递到main()还没处理,就交给JVM(直接终止程序))
4. 无论是否找到匹配的异常类型,finally都会被执行
3.自定义异常
虽然Java中提供了很多异常的类,但在实际开发中,我们需要面对不同的业务场景产生的异常,就需要自己定义一个异常类.
我们来观察一下,java中自带的异常怎么写的
1. 该类继承自Exception类/RuntimeException
2. 在构造方法里面调用父类的构造方法
比如:我们来自己写一个用户登录功能
我们手动抛出的是一个受查异常需要通过throws来向上抛或者try-chatch进行处理
整体代码:
class LogIn {
private String userName = "admin";
private String password = "123456";
public static void loginInfo(String userName, String password) throws UserNameException {
if (!userName.equals(userName)) {
throw new UserNameException("用户名错误");
}
if (!password.equals(password)) {
try {
throw new PasswordException("密码错误");
} catch (PasswordException e) {
e.printStackTrace();
}
} System.out.println("登陆成功");
}
public static void main(String[] args) throws UserNameException {
loginInfo("admin", "123456");
}
}
class UserNameException extends Exception {
public UserNameException(String message) {
super(message);
}
}
class PasswordException extends Exception {
public PasswordException(String message) {
super(message);
}
}
如果继承的是RuntimeException
class LogIn {
private String userName = "admin";
private String password = "123456";
public static void loginInfo(String userName, String password) {
if (!userName.equals(userName)) {
throw new UserNameException("用户名错误");
}
if (!password.equals(password)) {
throw new PasswordException("密码错误");
}
}
public static void main(String[] args) {
loginInfo("admin", "123456");
}
}
class UserNameException extends RuntimeException {
public UserNameException(String message) {
super(message);
}
}
class PasswordException extends RuntimeException {
public PasswordException(String message) {
super(message);
}
}
注意:
自定义异常通常会继承自 Exception 或者 RuntimeException
继承自 Exception 的异常默认是受查异常(必须要进行处理)
继承自 RuntimeException 的异常默认是非受查异常(可以不处理,直接交给jvm)
4. 一些注意的点
class Test2 {
public static void a () throws ArithmeticException{
int b = 1/0;
System.out.println("这个是处理完异常之后才会调用出来的代码");
}
public static void main(String[] args) {
try {
a();
}catch (ArithmeticException e){
e.printStackTrace();
}
System.out.println("之后的代码");
}
}
运行结果:
throws用在方法参数列表后面,注意的是,你如果调用该方法就必须对它进行处理,要么你把它继续往上抛,要么用try-catch来处理该异常.但是你如果都抛到了main方法里面,还想继续抛就直接给JVM处理,直接终止程序了,此时如果想打印在main里面调用该方法后面的代码就必须try-catch对该异常进行处理.