小白如何认识并处理Java异常?
目录
1.异常的概念和为什么要有处理异常
2.异常的体系结构
3.异常的分类
3.1 受检查异常(Checked Exceptions)
3.2 非受检查异常(Unchecked Exceptions)
4.异常的处理方式
5.异常处理的关键字
5.1 异常的抛出
5.2 异常的声明
5.3 异常的捕获和处理
5.4 finally
5.5 throw 和 throws 的区别
6.异常处理流程小结
7.实现自定义异常类
1.异常的概念和为什么要有处理异常
Java异常(Exception)是程序运行时发生的不正常情况,它会中断正常的指令流。异常机制是Java处理程序错误的标准方式,提供了一种结构化的、面向对象的方法来处理运行时问题。
正常的书面概念可能有点晦涩难懂,在这里可以想象一下:你通过手机APP点外卖的整个过程,正常的流程(没有异常)是这样的:
但是如果这个过程中某个环节出现意外,将导致你收不到外卖。这个过程中的各种意外情况就相当于程序中的"异常"。
当有这些异常之后,就要对其进行处理,为什么需要这些"异常处理"?
-
提前预防:就像APP会先检查你的网络连接再让你下单
-
优雅降级:当首选餐厅关门时,APP不会直接崩溃,而是推荐替代选项
-
明确反馈:不是简单显示"操作失败",而是告诉你具体原因(余额不足/商家打烊等)
-
资源清理:就像最终都会发送订单确认,确保流程完整
-
流程中断:当严重错误发生时(如支付失败),立即停止后续操作(不制作/不配送)
Java语言也是类似的,当编译器抛出一个异常时,就要对其进行有效处理。因此,认识Java的异常是非常有必要的,这样才能对异常对症下药。
2.异常的体系结构
下图是异常的体系结构重要的一部分,包括但不限于图中所有:
上图中可知:
- Throwable:是异常体系的顶层类,派生出Error和Exception这两个重要子类
- Error(错误):是指 Java 虚拟机无法解决的问题,比如 JVM 的内部错误等,典型的有OutOfMemoryError 和 StackOverflowError
- Exception(异常):当发生Exception时,程序🐒可以通过对代码进行处理,使其正常运行
3.异常的分类
异常有时候在编译时发生,有时候在程序运行时发生,前者称为受检查异常(Checked Exceptions),后者称为非受检查异常(Unchecked Exceptions)。
3.1 受检查异常(Checked Exceptions)
在程序编译时发生的异常,称为编译时异常,也称为受检查异常(Checked Exceptions)
常见的受检查异常:
( 1 ) IOException :
FileNotFoundException
:尝试打开不存在的文件
结果显示,受检查异常在编写代码时编译器就已经给出提示(FileReader下方标红),如果不对其进行处理,当运行时就会抛出异常:FileNotFoundException。
( 2 )SQLException:数据库操作异常
( 3 )ClassNotFoundException:类加载失败
3.2 非受检查异常(Unchecked Exceptions)
在编译时不发生,在程序运行时发生的异常,称为运行时异常,也成为非受检查异常(Unchecked Exceptions) 。
常见的非受检查异常:
( 1 )ArithmeticException:算术异常
结果显示,非受检查异常在编写代码是编译器不会给出标红提示,但是当运行时就会抛出异常:ArithmeticException,并且给出异常的位置和异常的原因(这里是0不能作为除数)。
( 2 )ArrayIndexOutOfBoundsException:数组越界异常
( 3 )NullPointerException:空指针异常
( 4 )ClassCastException:类型转换异常
4.异常的处理方式
认识异常之后,就先对异常进行处理。处理异常的主要方式有:
( 1 )LBYL(Look Before You Leap):在操作之前就做充分的检查,即事前防御型
// 典型LBYL代码结构
if (file.exists()) { // 先检查if (file.canRead()) { // 再检查// 最后执行操作String content = Files.readString(file.toPath());}
}
这种方式在在执行操作前显式检查所有可能出错的条件,正常流程和错误处理流程的代码混在一起,使得代码整体会显得比较混乱。因此处理异常时通常使用第二种方式:
( 2 )EAFP(Easier to Ask for Forgiveness than Permission):先执行操作,再处理可能的异常,而不是提前检查所有可能的错误条件,即事后认错型
try {// 可能抛出异常的操作
} catch (可以出现的哪类异常 e) {// 异常处理
} finally {// 可选的清理代码(无论是否异常都会执行)
}
这种方式的优势在于正常流程和错误流程是分开的,程序🐒编写代码时更关注正常流程,代码也更清晰。处理异常的核心思想便是EAFP。
要真正理解异常,就要认识异常处理的5个关键字: throw、try、catch、finally、throws。
5.异常处理的关键字
5.1 异常的抛出
在编写程序时,如果程序中出现错误,这个时候就需要把错误的信息告诉调用者。在Java中,使用关键字 throw ,抛出一个指定的异常对象,把错误的信息告诉调用者。
语法形式:
throw new XXXException("异常产生的原因")
如:
public class Main {public static int div(int x , int y) {if (0 == y) {throw new ArithmeticException("除数不能为0!!!");}System.out.println("你错了吗?");return x / y;}public static void main(String[] args) {int a = 666;int b = 0;System.out.println(div(a , b));}
}
这是一个实现两个数相除的方法,并通过 throw 抛出异常,来看运行结果:
结果显示, 这次给出的错误原因是程序🐒自己编写的,也就是“除数不能为0!!!”,并且可以看到“你错了吗?”并没有打印出来。
注意事项:
- throw 必须写在方法体内部
- 抛出的对象必须是Exception 或者 Exception 的子类对象
- 异常一旦抛出,后面的代码将不再执行
5.2 异常的声明
当编写代码时不想处理异常,可以借助关键字 throws 来声明异常,将异常抛给方法的调用者来处理,放在方法的参数列表之后。
语法形式:
修饰符 返回值类型 方法名 (参数列表)throws 异常类型1,异常类型2...{ }
public class Main {public static int getNum(int[] array , int index) throws NullPointerException,ArrayIndexOutOfBoundsException {if (null == array) {throw new NullPointerException("这个数组是空的!!!");}if (index < 0 || index >= array.length) {throw new ArrayIndexOutOfBoundsException("超出数组边界!!!");}return array[index];}public static void main(String[] args) {int[] array = {0 , 1 , 2 , 3 , 4};System.out.println(getNum(array ,6));}
}
在这个代码中,需要得到下标为index的数组元素,在写方法时通过关键字 throws 声明两个异常,即 NullPointerException 和 ArrayIndexOutOfBoundsException,来看运行结果:
结果显示,异常“超出数组边界 ”。
注意事项:
- throws 跟在方法的参数列表之后
- 声明的异常需要是 Exception 或者Exception 的子类
- 如果会抛出多个异常,throws 之后要跟多个异常类型,并且使用英文逗号隔开
5.3 异常的捕获和处理
throw 是抛出异常,throws 是声明异常,这两个关键字都没有对异常进行真正的处理,而是将异常原因抛出告诉给方法的调用者,再由调用者进行处理。如果要对异常进行真正的处理,就需要使用关键字 try 和 catch 。
语法形式:
try {//可能出现异常的代码 } catch (捕获的异常类型 e) {//如果抛出异常,在这里进行处理,处理完成后,跳出try-catch结构,继续执行后续代码 }finally {//这里的代码一定会被执行,后面继续介绍 } //后续代码,如果没有异常或者是异常被处理后,执行这里的代码,如果有异常但没有处理,这里就不会被执行
public class Main {public static void getNum(int[] array, int index) {try {System.out.println("异常之前的代码执行吗???");System.out.println("下标为" + index + "的元素是:" + array[index]);System.out.println("异常之后的代码执行吗????");} catch (NullPointerException e) {System.out.println("数组不能为null !!!");} catch (ArrayIndexOutOfBoundsException e) {System.out.println("索引越界!!!");}System.out.println("try-catch之后的代码执行了...");}public static void main(String[] args) {int[] array = null;getNum(array, 6); // 测试null数组System.out.println("--------分割线--------");array = new int[]{1, 2, 3};getNum(array, 6); // 测试越界索引}
}
在这个代码中,通过 catch 对异常进行处理,来看运行结果:
结果显示,捕获后的异常的 try-catch结构外的代码都被执行了。
注意事项:
- try 代码块中,如果有代码出现异常,那么这个异常代码之后的代码将不再执行
- 如果抛出异常类型与 catch 的异常类型不匹配,也就是说异常没有被成功捕获,那么就不会被处理,就会往外抛出异常
- try 中可能会有多个不同对象的异常,此时需要多个 catch 进行捕获
5.4 finally
在编写程序代码时,有一些特点的代码不论是否发生异常,都需要执行,比如程序中打开的资源如数据库链接、IO流,在程序正常或者退出时也要对资源进行回收,此时就需要关键字 finally 来构造一个代码块让这些代码执行起来。
前面说到 try-catch 时,语法形式是这样的:
语法形式:
try {//可能出现异常的代码 } catch (捕获的异常类型 e) {//如果抛出异常,在这里进行处理,处理完成后,跳出try-catch结构,继续执行后续代码 }finally {//这里的代码一定会被执行 } //后续代码,如果没有异常或者是异常被处理后,执行这里的代码,如果有异常但没有处理,这里就不会被执行
虽然在前面的结果中看到 try-catch 后续代码依然执行,但那是在处理异常之后的,如果没有对异常进行处理,那么 try-catch 后续代码是不会执行的,此时就突出了 finally 的重要性。
public class Main {public static void getNum(int[] array, int index) {try {System.out.println("异常之前的代码执行吗???");System.out.println("下标为" + index + "的元素是:" + array[index]);System.out.println("异常之后的代码执行吗????");} catch (NullPointerException e) {System.out.println("数组不能为null !!!");}//没有处理索引越界的异常处理finally {System.out.println("finally里面的代码块被执行了...");}System.out.println("try-catch之后的代码执行了...");}public static void main(String[] args) {int[] array = null;getNum(array, 6); // 测试null数组System.out.println("--------分割线--------");array = new int[]{1, 2, 3};getNum(array, 6); // 测试越界索引}
}
这个代码对数组索引超出边界时并没有对其进行异常处理,来看运行结果:
结果显示,不论异常是否处理,finally 的代码块一定会被执行!
5.5 throw 和 throws 的区别
异常处理的5个关键字throw、try、catch、finally、throws。其中throw 和 throws有什么区别呢?
(1)
throw
作用:主动抛出一个异常对象(在方法内部使用)
特点:
用于方法内部,当检测到错误条件时立即抛出异常
后面跟一个异常对象实例(
new ExceptionType()
)执行到
throw
时,当前方法立即停止,异常交给调用栈处理(2)
throws
作用:声明方法可能抛出的异常类型(在方法签名中使用)
特点:
用于方法声明处,告诉调用者该方法可能抛出哪些受检异常(Checked Exceptions)
后面跟异常类名(多个异常用英文逗号分隔)
不实际抛出异常,只是声明可能性
6.异常处理流程小结
- try 代码块最先执行
- 如果 try 代码块中没有出现异常,其代码块内的所有代码都会被执行,如何跳过 catch 的代码块;如果 try 代码块中的某一代码出现异常,则 该代码之后的 try 代码块将不会再执行,观察该异常是否和 catch 的异常是否匹配
- 如果找到匹配的异常类型,就会执行 catch 代码块中的代码
- 如果没有找到匹配的异常类型,就会将异常向上传递给上层调用者,如果向上一直传递没有合适的方法异常处理,最终会交给 JVM 处理,直到程序异常终止(和为会异常处理相似,直接抛出异常,有编译器给出提示,如果算术异常中“ by zero”)
7.实现自定义异常类
//自定义用户名异常类
public class NameException extends RuntimeException {public NameException(String message) {super(message);}
}
//自定义密码异常类
public class PassWordException extends RuntimeException {public PassWordException(String message) {super(message);}
}
// 用户登录类,封装登录逻辑
class UserLogin {// 用户属性private String name;// 用户名private String password;// 密码// Getter和Setter方法public String getName() {return name;}public void setName(String name) {this.name = name;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}/*** 用户登录验证方法* @param name 输入的用户名* @param password 输入的密码* @throws NameException 当用户名不匹配时抛出* @throws PassWordException 当密码不匹配时抛出*/public void userLogin(String name , String password) throws NameException, PassWordException {// 验证用户名if (! this.name.equals(name)) {throw new NameException("输入的用户名错误异常!");}// 验证密码if (! this.password.equals(password)) {throw new PassWordException("输入的密码错误异常!");}System.out.println("登陆成功"); // 验证通过}
}// 主程序类
public class Main {public static void main(String[] args) {//实现一个简单的控制台版用户登陆程序, 程序启动提示用户输入用户名密码. 如果用户名密码出错, 使用自定义异常的方式来处理UserLogin userLogin = new UserLogin();// 创建用户登录对象// 设置正确的用户名和密码(模拟数据库中的用户数据)userLogin.setName("zhangsan");userLogin.setPassword("12345");try {// 尝试登录(这里可以替换为从控制台获取的用户输入)userLogin.userLogin("zhangsan" , "12345");} catch (NameException e) {// 捕获并处理用户名异常e.printStackTrace();} catch (PassWordException e) {// 捕获并处理密码异常e.printStackTrace();}}
}
注意事项:
- 自定义异常类通常是继承Exception或者RuntimeException
- 继承自Exception的异常默认是受检查异常
- 继承自RuntimeException的异常默认是非受检查异常