今天和大家聊聊Java中最基础也最核心的知识点——异常处理。
不管是Java新手还是有一定经验的开发者,写代码时肯定都遇到过控制台爆红的情况:NullPointerException、ArrayIndexOutOfBoundsException、IOException……这些就是Java中的异常。很多新手遇到异常就慌了,不知道怎么排查、怎么解决;甚至有同学直接用try-catch“暴力包裹”,导致异常信息丢失、程序隐患暗藏。
这篇博客就从「异常是什么」「异常体系结构」「如何处理异常」「实战案例」「避坑指南」五个维度,把Java异常处理讲透,新手看完能上手,老手看完能避坑,建议收藏备用~
一、先搞懂:异常到底是什么?
异常(Exception),本质上是Java程序运行过程中出现的「不正常情况」,它会打断程序的正常执行流程。
举个生活化的例子:你计划从家开车去公司(正常执行流程),结果路上遇到爆胎(异常),如果不处理,车就开不了(程序终止);如果及时更换备胎(异常处理),就能继续赶往公司(程序正常执行)。
Java中的异常,就是程序中的“爆胎”——可能是代码逻辑错误、资源不足(比如文件找不到)、输入非法(比如让用户输入数字,结果输入了字符串)等原因导致的。
核心作用:捕获异常、处理异常,避免程序崩溃,同时定位问题根源,让程序更健壮、更易维护。
二、Java异常体系结构(重中之重)
Java中所有异常的顶层父类是 java.lang.Throwable,它有两个核心子类:Error(错误) 和 Exception(异常),二者的区别一定要分清!
2.1 顶层父类:Throwable
Throwable 定义了所有可被抛出(throw)的对象的共同属性和方法,核心方法有3个,必须记住:
-
getMessage():返回异常的详细描述信息(最常用,排查问题时优先看);
-
printStackTrace():打印异常的堆栈信息(包含异常发生的类、方法、行号,定位问题的关键);
-
toString():返回异常的类名+详细描述信息(不如 getMessage() 实用)。
2.2 Error(错误):程序无法处理的严重问题
Error 是虚拟机层面的严重错误,程序自身无法处理,只能提前预防,比如内存溢出、虚拟机崩溃等。
常见的 Error 案例:
-
StackOverflowError:栈溢出(比如递归调用没有终止条件,不断压栈导致);
-
OutOfMemoryError(OOM):内存溢出(比如创建大量对象不回收,耗尽JVM内存);
-
NoClassDefFoundError:类找不到错误(比如导入的依赖缺失、类路径配置错误)。
注意:不要试图用try-catch捕获Error,因为捕获后也无法恢复程序执行,重点是提前规避(比如控制递归深度、合理管理内存)。
2.3 Exception(异常):程序可以处理的问题
Exception 是我们日常开发中最常遇到的异常,也是异常处理的核心对象,程序可以通过try-catch等机制捕获并处理,处理后程序可以继续执行。
Exception 又分为两大类,根据「是否必须显式处理」来划分:
(1)受检异常(Checked Exception):必须显式处理
编译器会强制检查的异常,要么用try-catch捕获,要么用throws声明抛出,否则代码无法编译通过。
常见受检异常:
-
IOException:输入输出异常(比如读取文件、网络请求时);
-
SQLException:数据库操作异常(比如连接数据库、执行SQL时);
-
ClassNotFoundException:类找不到异常(比如反射加载类时);
-
ParseException:日期格式化异常(比如SimpleDateFormat解析非法日期字符串)。
(2)非受检异常(Unchecked Exception):无需显式处理
也叫运行时异常(RuntimeException),编译器不强制检查,即使不处理,代码也能编译通过,但运行时可能会报错。
这类异常大多是代码逻辑错误导致的,是我们开发中最需要注意避免的。
常见非受检异常:
-
NullPointerException(NPE):空指针异常(最常见!对象为null时调用方法/属性);
-
ArrayIndexOutOfBoundsException:数组下标越界异常(比如数组长度为3,访问下标3的元素);
-
ClassCastException:类型转换异常(比如把String类型强制转换成Integer类型);
-
IllegalArgumentException:非法参数异常(比如传递给方法的参数不符合要求);
-
ArithmeticException:算术异常(比如除以0)。
总结:异常体系核心区别
|
Error(错误) |
不可处理 |
否 |
OOM、栈溢出 |
|
受检异常 |
可处理 |
是 |
IOException、SQLException |
|
非受检异常 |
可处理 |
否 |
NPE、数组下标越界 |
三、异常处理的核心机制(4种方式)
Java提供了4种异常处理方式,分别是:try-catch、try-catch-finally、try-with-resources、throw/throws,重点掌握前3种,理解第4种。
3.1 try-catch:最基础的异常捕获
作用:捕获try块中可能出现的异常,并在catch块中处理异常,避免程序崩溃。
语法格式:
try {
// 可能出现异常的代码(核心代码)
} catch (异常类型1 异常变量名) {
// 处理异常类型1的逻辑(比如打印异常信息、提示用户)
} catch (异常类型2 异常变量名) {
// 处理异常类型2的逻辑
}
实战案例(捕获算术异常):
public class TryCatchDemo {
public static void main(String[] args) {
int a = 10;
int b = 0;
try {
// 可能出现异常的代码:除以0
int result = a / b;
System.out.println("计算结果:" + result);
} catch (ArithmeticException e) {
// 处理算术异常
System.out.println("异常原因:" + e.getMessage()); // 输出:/ by zero
e.printStackTrace(); // 打印堆栈信息,定位问题
}
// 异常处理后,程序继续执行
System.out.println("程序执行完毕");
}
}
关键注意点:
-
try块中代码越少越好,只包裹“可能出现异常的核心代码”,避免冗余;
-
多个catch块捕获不同异常时,异常类型必须从小到大(子类在前,父类在后),否则会报错;
-
比如:先catch(ArithmeticException),再catch(Exception),不能反过来(Exception是所有异常的父类,会覆盖子类异常)。
3.2 try-catch-finally:异常处理+资源释放
finally块的核心作用:无论try块是否出现异常、catch块是否执行,finally块中的代码一定会执行,常用于释放资源(比如关闭文件、关闭数据库连接、关闭流)。
语法格式:
try {
// 可能出现异常的代码
} catch (异常类型 异常变量名) {
// 处理异常
} finally {
// 必须执行的代码(资源释放)
}
实战案例(读取文件,关闭流资源):
import java.io.FileInputStream;
import java.io.IOException;
public class TryCatchFinallyDemo {
public static void main(String[] args) {
FileInputStream fis = null;
try {
// 读取文件(可能出现IOException)
fis = new FileInputStream("test.txt");
int data = fis.read();
System.out.println("读取到的数据:" + data);
} catch (IOException e) {
System.out.println("文件读取异常:" + e.getMessage());
e.printStackTrace();
} finally {
// 释放资源:关闭流(必须执行,即使出现异常)
try {
if (fis != null) { // 避免空指针异常(fis未初始化成功时为null)
fis.close();
System.out.println("流资源已关闭");
}
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("程序执行完毕");
}
}
关键注意点:
-
finally块中尽量不要使用return语句,会覆盖try/catch块中的return值,导致异常信息丢失;
-
如果try块中出现异常,且catch块中没有捕获到(比如异常类型不匹配),finally块依然会执行,执行完毕后程序才会崩溃。
3.3 try-with-resources:自动释放资源(Java7+新特性)
上面的try-catch-finally释放资源,代码比较繁琐(需要在finally中再次try-catch)。Java7引入了try-with-resources语法,自动释放实现了AutoCloseable接口的资源(比如流、数据库连接、Socket等),无需手动关闭,代码更简洁。
语法格式:
try (资源声明; 资源声明) {
// 可能出现异常的代码(使用资源)
} catch (异常类型 异常变量名) {
// 处理异常
}
注意:资源声明时,多个资源用分号分隔,Java会自动按“声明顺序的反向”关闭资源。
实战案例(简化文件读取,自动关闭流):
import java.io.FileInputStream;
import java.io.IOException;
public class TryWithResourcesDemo {
public static void main(String[] args) {
// try-with-resources:自动关闭FileInputStream(实现了AutoCloseable接口)
try (FileInputStream fis = new FileInputStream("test.txt")) {
int data = fis.read();
System.out.println("读取到的数据:" + data);
} catch (IOException e) {
System.out.println("文件读取异常:" + e.getMessage());
e.printStackTrace();
}
// 无需手动关闭流,Java自动释放
System.out.println("程序执行完毕");
}
}
优势:代码简洁,避免手动关闭资源遗漏,减少空指针和异常嵌套,是目前开发中推荐的方式(Java7及以上可用)。
3.4 throw与throws:手动抛出异常+声明异常
除了Java自动抛出的异常(比如除以0抛出ArithmeticException),我们也可以手动抛出异常,或者声明方法可能抛出的异常,用于自定义异常场景。
(1)throw:手动抛出异常(方法内部)
语法:throw new 异常类型(异常描述信息);
实战案例(判断参数合法性,手动抛出异常):
public class ThrowDemo {
// 计算两个数的和,要求参数不能为负数
public static int add(int a, int b) {
// 手动判断参数,不合法则抛出异常
if (a < 0 || b < 0) {
throw new IllegalArgumentException("参数不能为负数!"); // 手动抛出非受检异常
}
return a + b;
}
public static void main(String[] args) {
try {
int result = add(-1, 2);
System.out.println("和为:" + result);
} catch (IllegalArgumentException e) {
System.out.println("异常:" + e.getMessage()); // 输出:参数不能为负数!
}
}
}
(2)throws:声明方法可能抛出的异常(方法声明处)
作用:告诉调用者“这个方法可能会抛出这些异常,请你处理”,常用于受检异常(编译器强制要求)。
语法:public 返回值类型 方法名(参数列表) throws 异常类型1, 异常类型2 { … }
实战案例(声明受检异常,由调用者处理):
import java.io.IOException;
public class ThrowsDemo {
// 声明方法可能抛出IOException(受检异常),由调用者处理
public static void readFile() throws IOException {
FileInputStream fis = new FileInputStream("test.txt");
fis.read();
fis.close(); // 这里的close()也可能抛出IOException,无需单独处理,由throws声明
}
public static void main(String[] args) {
// 调用readFile(),必须处理其声明的IOException(try-catch或继续throws)
try {
readFile();
} catch (IOException e) {
System.out.println("文件读取异常:" + e.getMessage());
}
}
}
(3)throw与throws的区别
|
throw |
方法内部 |
手动抛出一个具体的异常对象 |
单个异常 |
|
throws |
方法声明处 |
声明方法可能抛出的异常类型 |
多个异常(用逗号分隔) |
四、自定义异常(实战必备)
Java自带的异常(比如NullPointerException、IOException)虽然能覆盖大部分场景,但在实际开发中,我们经常需要自定义异常,来描述业务场景中的“异常情况”(比如用户不存在、密码错误、订单状态异常等)。
自定义异常的核心:继承Exception(受检异常)或RuntimeException(非受检异常),并重写构造方法(无参、带消息的构造方法)。
4.1 自定义异常步骤(以“用户不存在”为例)
创建异常类,继承Exception或RuntimeException;
重写无参构造方法和带String消息的构造方法;
在业务逻辑中,用throw手动抛出自定义异常;
调用方法时,处理自定义异常(try-catch或throws)。
4.2 实战案例:自定义UserNotExistException
// 1. 自定义异常(继承RuntimeException,非受检异常,无需强制处理)
public class UserNotExistException extends RuntimeException {
// 无参构造方法
public UserNotExistException() {
super();
}
// 带消息的构造方法(核心,传递异常描述)
public UserNotExistException(String message) {
super(message);
}
}
// 2. 业务逻辑类(模拟用户查询)
class UserService {
// 模拟根据用户名查询用户
public void findUser(String username) {
// 模拟业务逻辑:用户名不存在
if (!"admin".equals(username)) {
// 手动抛出自定义异常
throw new UserNotExistException("用户名:" + username + " 不存在!");
}
System.out.println("查询到用户:" + username);
}
}
// 3. 测试类
public class CustomExceptionDemo {
public static void main(String[] args) {
UserService userService = new UserService();
try {
userService.findUser("test"); // 传递不存在的用户名
} catch (UserNotExistException e) {
// 处理自定义异常
System.out.println("业务异常:" + e.getMessage()); // 输出:用户名:test 不存在!
e.printStackTrace();
}
}
}
注意:
-
如果自定义异常是受检异常(继承Exception),则抛出时必须用throws声明,或用try-catch捕获;
-
如果是非受检异常(继承RuntimeException),则无需强制处理,更适合业务逻辑异常(推荐使用,更灵活)。
五、异常处理最佳实践(避坑指南)
很多开发者虽然会用异常处理,但写法不规范,导致程序隐患、排查困难。以下5个最佳实践,一定要牢记!
1. 不要捕获所有异常(catch (Exception e))
错误示例:
try {
// 核心代码
} catch (Exception e) {
// 捕获所有异常,无法定位具体问题
System.out.println("出现异常了");
}
问题:捕获所有异常,会掩盖真正的异常类型(比如把NPE和IOException混为一谈),排查时无法快速定位问题根源。
正确做法:捕获具体的异常类型,只捕获自己能处理的异常,无法处理的异常要么throws抛出,要么交给上层处理。
2. 异常信息要具体,不要空泛
错误示例:throw new RuntimeException("异常了");
正确示例:throw new RuntimeException("用户名不能为空,当前用户名为:" + username);
原因:具体的异常信息能大幅提升排查效率,让开发者一眼知道问题出在哪里。
3. 避免在finally中使用return
错误示例:
public static int test() {
try {
return 1;
} catch (Exception e) {
return 2;
} finally {
return 3; // 错误:finally中的return会覆盖try/catch的return值
}
}
运行结果:返回3,而非1,导致try块的return值被覆盖,逻辑异常。
4. 优先使用try-with-resources释放资源
替代传统的try-catch-finally关闭资源,减少代码冗余,避免资源泄漏(比如流未关闭导致的文件占用)。
5. 非受检异常(RuntimeException)用于逻辑错误,受检异常用于外部资源异常
-
非受检异常:比如用户输入非法、参数错误(逻辑问题,提前规避);
-
受检异常:比如文件读取、网络请求、数据库操作(外部资源问题,必须处理)。
六、常见异常排查技巧(新手必看)
先看异常信息的第一行:明确异常类型(比如NullPointerException)和异常描述(getMessage());
再看异常的堆栈信息(printStackTrace()输出):找到“at 包名.类名.方法名(文件名.java:行号)”,定位到异常发生的具体代码行;
排查代码行周围的变量:比如NPE,就看该行的对象是否为null;数组下标越界,就看数组长度和访问的下标;
如果是受检异常(比如IOException),优先检查外部资源:文件是否存在、路径是否正确、权限是否足够。
七、总结
Java异常处理的核心,不是“捕获所有异常”,而是“合理捕获、正确处理、准确定位”。
记住3个核心点:
-
异常体系:Throwable → Error(不可处理)、Exception(可处理);
-
处理方式:try-catch(基础)、try-catch-finally(资源释放)、try-with-resources(推荐);
-
最佳实践:捕获具体异常、异常信息具体、优先自动释放资源、避免finallyreturn。
异常处理是Java开发的基础,也是程序健壮性的关键。新手刚开始可以多写案例、多踩坑,慢慢就能掌握其中的技巧。
如果这篇博客对你有帮助,欢迎点赞、收藏、评论,有任何疑问可以在评论区留言,我会一一回复~ 关注我,后续分享更多Java实战知识点!





