在 java 开发的道路上,异常处理是绕不开的核心知识点。无论是新手调试代码时遇到的nullpointerexception,还是开发企业级项目时处理的网络、文件 io 异常,掌握规范的异常处理方式,能让我们的代码更健壮、更易维护,还能大幅降低线上问题的排查成本。本文将从异常的核心概念出发,逐步拆解 java 异常体系、处理方式、执行流程,最后手把手教你实现自定义异常,让你彻底吃透 java 异常处理。
一、什么是异常?打破程序的正常流程
异常是程序运行过程中发生的非正常行为 / 错误状态,它会打断程序的正常执行流程,需要开发者通过特定方式进行处理。
在开发中,我们无法避免各类异常场景:比如除数为 0 的算术错误、访问数组不存在的下标、调用空对象的方法、读取不存在的文件、网络请求超时等。这些场景无法通过普通的逻辑判断完全规避,而 java 为每一种异常场景都提供了对应的类来描述,让我们能精准定位和处理问题。
常见异常示例
/**
* 常见运行时异常演示
*/
public class commonexceptiondemo {
public static void main(string[] args) {
// 1. 算术异常:arithmeticexception
int a = 10;
int b = 0;
// system.out.println(a / b);
// 2. 数组越界异常:arrayindexoutofboundsexception
string[] strarr = {"java", "python", "c++"};
// system.out.println(strarr[5]);
// 3. 空指针异常:nullpointerexception
string str = null;
// system.out.println(str.length());
// 4. 类型转换异常:classcastexception
object obj = new integer(100);
// system.out.println((string) obj);
}
}取消上述代码的注释,运行后会看到控制台抛出对应的异常信息,包含异常类型、异常原因和出错的代码行,这也是 java 异常给我们的核心调试线索。
二、java 异常体系结构:理清继承关系,不混淆概念
java 为了对不同类型的异常和错误进行分类管理,设计了一套清晰的异常体系,其顶层类是java.lang.throwable,所有异常和错误都直接或间接继承自该类。
核心继承结构
throwable
├─ error:jvm级别的严重错误,无法通过代码处理
│ ├─ stackoverflowerror:栈溢出错误(如递归无终止条件)
│ └─ outofmemoryerror:内存溢出错误(oom)
└─ exception:程序级别的异常,开发者可通过代码处理
├─ 编译时异常(受检查异常 checked exception):编译期必须处理
│ ├─ ioexception:io操作相关异常(如文件读取、网络请求)
│ ├─ sqlexception:数据库操作相关异常
│ └─ classnotfoundexception:类加载失败异常
└─ 运行时异常(非受检查异常 unchecked exception):运行期才会出现,可按需处理
├─ nullpointerexception:空指针异常
├─ arrayindexoutofboundsexception:数组越界异常
├─ arithmeticexception:算术异常
└─ classcastexception:类型转换异常
三大核心类的区别
- error:java 虚拟机无法解决的严重问题,属于系统级错误,一旦发生程序基本无法恢复,只能提前预防。比如递归调用无终止条件会导致
stackoverflowerror,创建大量对象未释放会导致outofmemoryerror。 - 编译时异常(checked exception):在程序编译阶段就会被检测到的异常,编译器强制要求开发者必须处理(捕获或抛出),否则代码无法通过编译。比如读取文件时的
filenotfoundexception,必须显式处理。 - 运行时异常(unchecked exception):继承自
runtimeexception的异常,在程序运行阶段才会触发,编译器不强制处理。这类异常通常是由于开发者的代码逻辑错误导致的,比如空指针、数组越界,建议通过优化代码逻辑避免,而非被动处理。
重要区分:编译期的语法错误(如把system.out.println写成system.out.println)不属于异常,只是代码书写错误,编译器会直接提示,无法生成 class 文件;而异常是代码编译通过后,jvm 执行时发生的错误。
三、异常的处理方式:从防御式编程到核心关键字
在处理异常前,我们需要了解两种编程思想:事前防御和事后处理,java 异常处理的核心基于后者,同时提供了 5 个核心关键字:throw、throws、try、catch、finally,掌握这 5 个关键字,就能处理绝大多数异常场景。
3.1 两种防御式编程思想
lbyl:事前防御型(look before you leap)
在执行操作前,对所有可能出现的问题进行充分检查,正常流程和错误处理流程混在一起,代码可读性差。
/**
* 事前防御型编程示例:用户登陆
*/
public class lbyldemo {
public static void main(string[] args) {
string username = "test";
string password = "123";
// 检查用户名是否为空
if (username == null || username.isempty()) {
system.out.println("错误:用户名为空");
return;
}
// 检查密码是否为空
if (password == null || password.isempty()) {
system.out.println("错误:密码为空");
return;
}
// 检查用户名密码是否正确
if (!"admin".equals(username) || !"admin123".equals(password)) {
system.out.println("错误:用户名或密码错误");
return;
}
system.out.println("登陆成功");
}
}缺陷:代码中大量的if判断让核心业务逻辑被淹没,后期维护难度大。
eafp:事后处理型(it's easier to ask forgiveness than permission)
先执行操作,遇到问题再捕获处理,将正常流程和错误流程分离,代码更清晰,这也是 java 异常处理的核心思想。
/**
* 事后处理型编程示例:用户登陆
*/
public class eafpdemo {
public static void main(string[] args) {
string username = "test";
string password = "123";
try {
// 直接执行核心业务逻辑,不做前置检查
login(username, password);
system.out.println("登陆成功");
} catch (nullpointerexception e) {
system.out.println("错误:用户名或密码为空");
} catch (illegalargumentexception e) {
system.out.println("错误:" + e.getmessage());
}
}
private static void login(string username, string password) {
if (username == null || password == null) {
throw new nullpointerexception();
}
if (!"admin".equals(username) || !"admin123".equals(password)) {
throw new illegalargumentexception("用户名或密码错误");
}
}
}优势:核心业务逻辑login()方法简洁,错误处理集中在catch块,开发者更关注正常流程,代码可读性和可维护性大幅提升。
3.2 手动抛出异常:throw
在编写程序时,如果检测到非法的业务逻辑或参数错误,需要主动将错误信息告知调用者,此时可以使用throw关键字手动抛出一个指定的异常对象。
语法格式
throw new 异常类名("异常产生的原因");
实战示例:参数合法性校验
/**
* throw 手动抛出异常示例:获取集合指定下标元素
*/
import java.util.list;
public class throwdemo {
public static <t> t getlistelement(list<t> list, int index) {
// 校验集合是否为null
if (list == null) {
throw new nullpointerexception("传递的集合对象为null,无法获取元素");
}
// 校验下标是否合法
if (index < 0 || index >= list.size()) {
throw new indexoutofboundsexception("传递的下标" + index + "越界,集合长度为" + list.size());
}
// 校验通过,返回元素
return list.get(index);
}
public static void main(string[] args) {
list<string> list = list.of("java", "异常", "处理");
// 正常获取
system.out.println(getlistelement(list, 1));
// 下标越界,手动抛出异常
// system.out.println(getlistelement(list, 5));
// 集合为null,手动抛出异常
// system.out.println(getlistelement(null, 0));
}
}throw 使用注意事项
throw必须写在方法体内部;- 抛出的对象必须是
exception或其子类的实例; - 抛出运行时异常(如
nullpointerexception),调用者可按需处理,编译器不强制; - 抛出编译时异常(如
ioexception),调用者必须处理(捕获或抛出),否则代码无法编译; - 异常一旦抛出,其后的代码将不会执行。
3.3 声明异常:throws
如果当前方法没有能力处理抛出的异常,或者希望将异常处理的责任转移给调用者,此时可以使用throws关键字在方法声明处声明该方法可能抛出的异常。
语法格式
修饰符 返回值类型 方法名(参数列表) throws 异常类型1, 异常类型2... {
// 方法体,可能抛出异常
}
实战示例:文件操作声明编译时异常
/**
* throws 声明异常示例:文件读取
*/
import java.io.file;
import java.io.filereader;
import java.io.filenotfoundexception;
public class throwsdemo {
// 声明文件未找到异常,交给调用者处理
public static filereader openfile(string filepath) throws filenotfoundexception {
file file = new file(filepath);
// filenotfoundexception是编译时异常,此处不处理,声明后抛出
return new filereader(file);
}
public static void main(string[] args) {
try {
// 调用声明异常的方法,必须处理异常
filereader fr = openfile("test.txt");
system.out.println("文件打开成功");
fr.close();
} catch (filenotfoundexception e) {
system.out.println("异常原因:" + e.getmessage());
}
}
}throws 使用注意事项
throws必须跟在方法参数列表之后;- 声明的异常必须是
exception或其子类; - 方法内部抛出多个异常时,
throws后用逗号分隔多个异常类型;若异常之间有父子关系,直接声明父类异常即可(如filenotfoundexception继承自ioexception,可直接声明throws ioexception); - 调用声明了编译时异常的方法,调用者必须处理(
try-catch捕获或继续throws抛出);调用声明了运行时异常的方法,编译器不强制处理。
3.4 捕获并处理异常:try-catch
throws只是将异常转移给调用者,并未真正处理异常;而try-catch是 java 中处理异常的核心方式,能捕获异常并对其进行处理,让程序在发生异常后继续执行。
语法格式
try {
// 可能抛出异常的代码块(监控区)
} catch (异常类型1 异常对象名) {
// 处理异常类型1的代码(捕获区)
} catch (异常类型2 异常对象名) {
// 处理异常类型2的代码
}
// 可选:finally块,下文单独讲解
实战示例:多异常捕获与处理
/**
* try-catch 捕获异常示例:多异常处理
*/
public class trycatchdemo {
public static void calculate(int a, int b, int[] arr) {
try {
system.out.println("a / b = " + (a / b));
system.out.println("数组下标0的元素:" + arr[0]);
} catch (arithmeticexception e) {
// 处理算术异常
system.out.println("处理算术异常:" + e.getmessage());
} catch (nullpointerexception e) {
// 处理空指针异常
system.out.println("处理空指针异常:数组对象为null");
} catch (arrayindexoutofboundsexception e) {
// 处理数组越界异常
system.out.println("处理数组越界异常:" + e.getmessage());
}
}
public static void main(string[] args) {
// 测试1:除数为0
calculate(10, 0, new int[]{1,2});
system.out.println("===== 分割线 =====");
// 测试2:数组为null
calculate(10, 2, null);
system.out.println("===== 分割线 =====");
// 测试3:数组越界(空数组)
calculate(10, 2, new int[]{});
// 异常处理后,后续代码正常执行
system.out.println("程序执行完成");
}
}运行结果
处理算术异常:/ by zero ===== 分割线 ===== 处理空指针异常:数组对象为null ===== 分割线 ===== 处理数组越界异常:index 0 out of bounds for length 0 程序执行完成
可以看到,即使发生了异常,经过try-catch处理后,程序的后续代码依然能正常执行,这也是异常处理的核心目的。
try-catch 使用关键注意事项
try块中抛出异常的位置后续代码不会执行;- 异常捕获遵循类型匹配原则:只有
catch的异常类型与try中抛出的异常类型一致,或为其父类,才能捕获到异常; - 处理多个不同类型的异常时,需注意子类异常在前,父类异常在后,否则会出现语法错误(父类异常会捕获所有子类异常,后续的子类异常
catch块永远无法执行); - 若多个异常的处理逻辑完全相同,可使用 ** 竖线 |** 合并捕获,简化代码:
catch (arithmeticexception | nullpointerexception | arrayindexoutofboundsexception e) { system.out.println("处理异常:" + e.getmessage()); } - 可以使用
exception捕获所有异常(因为exception是所有程序级异常的父类),但不推荐:会掩盖具体的异常类型,不利于问题排查,仅适用于通用的异常兜底处理。
3.5 必执行的代码块:finally
在程序开发中,有些代码无论是否发生异常,都必须执行,比如打开的文件流、数据库连接、网络连接等资源的释放,否则会造成资源泄漏。finally块就是为了解决这个问题,它配合try-catch使用,里面的代码永远会被执行。
语法格式
try {
// 可能抛出异常的代码
} catch (异常类型 e) {
// 处理异常的代码
} finally {
// 无论是否发生异常,都会执行的代码(资源释放为主)
}
核心场景:资源释放(全新示例)
/**
* finally 块示例:资源释放(scanner)
*/
import java.util.scanner;
import java.util.inputmismatchexception;
public class finallydemo {
public static int getintinput() {
scanner sc = new scanner(system.in);
try {
system.out.print("请输入一个整数:");
// 尝试获取整数输入
int num = sc.nextint();
return num;
} catch (inputmismatchexception e) {
system.out.println("输入类型错误,不是整数");
return -1;
} finally {
// 无论是否输入正确,都关闭scanner,释放资源
system.out.println("执行finally块:关闭scanner资源");
sc.close();
}
}
public static void main(string[] args) {
int num = getintinput();
system.out.println("获取到的数字:" + num);
}
}测试结果 1:输入正确整数
请输入一个整数:100 执行finally块:关闭scanner资源 获取到的数字:100
测试结果 2:输入非整数
请输入一个整数:abc 输入类型错误,不是整数 执行finally块:关闭scanner资源 获取到的数字:-1
可以看到,无论try块中是否发生异常,finally块的代码都会执行,完美解决了资源释放的问题。
finally 的特殊注意事项
finally块的执行时机:在方法返回之前(即使try或catch中有return语句,也会先执行finally块,再执行return);- 若
finally块中也有return语句,会覆盖try或catch中的return结果,强烈不建议在finally中写return(编译器会给出警告); finally块唯一不执行的情况:程序执行到try/catch块时,调用了system.exit(0)(强制终止 jvm),此时 jvm 直接退出,所有代码都不再执行。
四、异常的处理流程:跟着调用栈走,理清执行顺序
要彻底理解异常处理,必须理清异常的传播和处理流程,而核心就是方法调用栈:java 中方法之间的调用关系会被 jvm 存储在虚拟机栈中,当发生异常时,异常会沿着调用栈从下往上传播,直到被捕获处理,若最终无人处理,则由 jvm 接管,程序异常终止。
异常处理完整执行流程
- 程序先执行
try块中的代码; - 若
try块中未发生异常,跳过catch块,直接执行finally块,再执行try-catch-finally后的代码; - 若
try块中发生异常,立即终止try块后续代码,匹配catch块的异常类型:- 找到匹配的异常类型:执行对应
catch块的处理代码,再执行finally块,最后执行后续代码; - 未找到匹配的异常类型:先执行
finally块,再将异常向上传播给上层调用者;
- 找到匹配的异常类型:执行对应
- 上层调用者重复步骤 3,若所有调用者都未处理异常,最终传递到
main方法; - 若
main方法也未处理异常,异常会被jvm 接管,jvm 会打印异常信息(类型、原因、调用栈),并强制终止程序,main方法后续代码不再执行。
实战示例:异常的向上传播
/**
* 异常处理流程示例:异常向上传播
*/
public class exceptionflowdemo {
// 方法3:抛出数组越界异常
public static void method3() {
int[] arr = {1,2,3};
system.out.println(arr[10]); // 抛出异常
}
// 方法2:调用method3,未处理异常
public static void method2() {
method3();
}
// 方法1:调用method2,捕获并处理异常
public static void method1() {
try {
method2();
} catch (arrayindexoutofboundsexception e) {
system.out.println("method1捕获到异常:" + e.getmessage());
}
}
public static void main(string[] args) {
method1();
// 异常被处理,后续代码正常执行
system.out.println("main方法后续代码执行");
}
}运行结果
method1捕获到异常:index 10 out of bounds for length 3
main方法后续代码执行
若删除method1中的try-catch,异常会传播到main方法,若main方法也不处理,jvm 会接管,程序终止,main方法后续代码不再执行。
五、自定义异常:贴合业务场景,让异常更有意义
java 内置了丰富的异常类,但这些异常类都是通用的,无法精准描述实际开发中的业务异常,比如用户登陆时的 “用户名不存在”、“密码错误”,订单操作时的 “订单不存在”、“库存不足” 等。此时我们需要自定义异常类,贴合业务场景,让异常信息更精准,便于问题排查和业务处理。
5.1 自定义异常的实现步骤
java 中自定义异常的核心是继承,遵循以下两步即可:
- 自定义异常类,继承自
exception(编译时异常,受检查)或runtimeexception(运行时异常,非受检查); - 实现带 string 类型参数的构造方法,将异常原因通过
super()传递给父类构造方法(便于通过getmessage()获取异常原因)。
5.2 实战示例:用户登陆业务的自定义异常
我们针对用户登陆场景,自定义两个业务异常:usernamenotexistexception(用户名不存在)、passworderrorexception(密码错误),并在业务代码中抛出和处理。
步骤 1:实现自定义异常类
/**
* 自定义异常:用户名不存在(继承exception,编译时异常)
*/
public class usernamenotexistexception extends exception {
// 构造方法,传递异常原因
public usernamenotexistexception(string message) {
super(message);
}
}
/**
* 自定义异常:密码错误(继承exception,编译时异常)
*/
public class passworderrorexception extends exception {
public passworderrorexception(string message) {
super(message);
}
}可选优化:若希望自定义异常为运行时异常,只需将父类改为runtimeexception,编译器不强制处理。
步骤 2:在业务代码中抛出自定义异常
/**
* 用户登陆业务类
*/
public class userloginservice {
// 模拟数据库中的用户信息
private static final string db_username = "admin";
private static final string db_password = "admin123456";
/**
* 登陆方法,抛出自定义业务异常
* @param username 用户名
* @param password 密码
* @throws usernamenotexistexception 用户名不存在
* @throws passworderrorexception 密码错误
*/
public void login(string username, string password) throws usernamenotexistexception, passworderrorexception {
// 校验用户名
if (!db_username.equals(username)) {
throw new usernamenotexistexception("用户名[" + username + "]不存在");
}
// 校验密码
if (!db_password.equals(password)) {
throw new passworderrorexception("密码错误,请重新输入");
}
}
}步骤 3:调用业务方法,处理自定义异常
/**
* 测试自定义异常:用户登陆
*/
public class customexceptiontest {
public static void main(string[] args) {
userloginservice loginservice = new userloginservice();
// 测试1:用户名不存在
string username = "test";
string password = "admin123456";
try {
loginservice.login(username, password);
system.out.println("登陆成功!");
} catch (usernamenotexistexception e) {
system.out.println("登陆失败:" + e.getmessage());
// 可做后续处理,如跳转到注册页面
} catch (passworderrorexception e) {
system.out.println("登陆失败:" + e.getmessage());
// 可做后续处理,如提示密码找回
}
// 测试2:密码错误
system.out.println("===== 分割线 =====");
username = "admin";
password = "123";
try {
loginservice.login(username, password);
system.out.println("登陆成功!");
} catch (usernamenotexistexception | passworderrorexception e) {
system.out.println("登陆失败:" + e.getmessage());
}
}
}运行结果
登陆失败:用户名[test]不存在
===== 分割线 =====
登陆失败:密码错误,请重新输入
可以看到,自定义异常能精准描述业务中的错误场景,让异常处理更贴合实际业务,同时异常信息更直观,便于开发和运维人员排查问题。
5.3 自定义异常的选型建议
- 若希望编译器强制处理该异常(如核心业务异常,必须显式处理),让自定义异常继承
exception(编译时异常); - 若该异常可通过代码逻辑避免,或希望简化代码(不强制处理),让自定义异常继承
runtimeexception(运行时异常); - 自定义异常的命名要见名知意,通常以
exception结尾,如ordernotexistexception、stocknotenoughexception。
六、异常处理的最佳实践
掌握了异常的基础知识点后,更重要的是在实际开发中遵循最佳实践,让异常处理更规范、更高效:
- 避免捕获所有异常:不要直接捕获
exception,会掩盖具体的异常类型,不利于问题排查,应捕获具体的异常类型; - 不要忽略异常:不要在
catch块中只写e.printstacktrace(),甚至空的catch块,应根据业务场景做具体处理(如记录日志、提示用户、重试操作); - 及时释放资源:打开的 io 流、数据库连接、网络连接等资源,必须在
finally块中释放,或使用 java7 的try-with-resources自动释放; - 异常信息要精准:抛出异常时,填写清晰的异常原因(如
throw new nullpointerexception("用户信息对象为null,无法获取用户id")),便于排查问题; - 子类方法抛出异常范围不超过父类:继承父类并重写方法时,子类方法抛出的异常类型不能是父类方法异常的父类,也不能抛出更多的受检查异常;
- 合理选择自定义异常的父类:核心业务异常建议继承
exception,强制调用者处理;非核心异常建议继承runtimeexception,简化代码; - 使用日志框架记录异常:实际开发中,不要使用
e.printstacktrace(),应使用 slf4j/logback 等日志框架记录异常(可记录异常级别、调用栈、业务上下文),便于线上问题排查。
七、总结
java 异常处理是保证程序健壮性的核心,其核心是事后处理的编程思想,通过throw、throws、try、catch、finally五个关键字实现异常的抛出、声明、捕获和处理。
本文从异常的概念出发,理清了throwable、error、exception的继承关系,区分了编译时异常和运行时异常;然后详细讲解了异常处理的核心方式和执行流程;最后通过实战实现了贴合业务的自定义异常,并给出了开发中的最佳实践。
掌握异常处理的关键,不仅是记住语法和规则,更重要的是结合业务场景选择合适的处理方式:让程序在发生异常时,既能精准定位问题,又能优雅地处理异常,保证程序的正常运行。希望本文能让你对 java 异常处理有更全面、更深入的理解,在实际开发中玩转异常体系!
到此这篇关于java入门异常处理最佳实践的文章就介绍到这了,更多相关java异常处理内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论