目录
[1.1 异常的概念](#1.1 异常的概念)
[1.2 异常的体系结构](#1.2 异常的体系结构)
[1.3 异常的分类](#1.3 异常的分类)
[2.1 防御式编程的两种思路](#2.1 防御式编程的两种思路)
[2.2 抛出异常:throw](#2.2 抛出异常:throw)
[2.3 声明异常:throws](#2.3 声明异常:throws)
[2.4 捕获异常:try-catch](#2.4 捕获异常:try-catch)
[2.5 finally:无论是否异常都要执行](#2.5 finally:无论是否异常都要执行)
[2.6 异常的处理流程](#2.6 异常的处理流程)
[3.1 实现自定义异常](#3.1 实现自定义异常)
[3.2 注意事项](#3.2 注意事项)
写在前面
相信每一位Java初学者都曾经历过这样的时刻:满怀信心地写完代码,点击运行,结果控制台突然冒出一片刺眼的红色错误信息。程序崩溃了,而你一脸茫然地看着那个陌生的异常类名,不知道发生了什么,更不知道该如何解决。
异常,是编程路上绕不开的一道坎。但换个角度看,异常其实是程序在"说话"------它在告诉你:"我这里出问题了,快来帮帮我。"
今天这篇文章,我们就来系统地学习Java的异常处理机制。掌握了它,你就能听懂程序在说什么,并且知道该怎么回应。
一、什么是异常?
1.1 异常的概念
异常是指在程序执行过程中发生的一种特殊情况或错误状态。它打断了程序的正常执行流程,需要特别处理。
在日常开发中,即使程序员绞尽脑汁想把代码写得尽善尽美,程序运行过程中还是难免会出现各种问题。有些问题通过代码很难完全控制,比如数据格式不对、网络不通畅、内存不足等。
在Java中,程序执行过程中发生的不正常行为被称为异常。不同类型的异常,都有对应的类来描述。
比如下面这些你一定见过:
// 算术异常
System.out.println(10 / 0);
// Exception in thread "main" java.lang.ArithmeticException: / by zero
// 数组越界异常
int[] array = {1, 2, 3};
System.out.println(array[100]);
// Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
// 空指针异常
String str = null;
System.out.println(str.length());
// Exception in thread "main" java.lang.NullPointerException
1.2 异常的体系结构
Java内部维护了一套完善的异常体系结构,对各种异常和错误进行分类管理:
Throwable(顶层父类)
├── Error(严重问题,程序无法处理)
│ ├── StackOverflowError
│ └── OutOfMemoryError
└── Exception(可处理的异常)
├── RuntimeException(运行时异常/非受查异常)
│ ├── NullPointerException
│ ├── ArrayIndexOutOfBoundsException
│ └── ArithmeticException
└── 其他受查异常
├── IOException
├── SQLException
└── FileNotFoundException
关键理解:
-
Throwable:异常体系的顶层类,派生出Error和Exception两个子类
-
Error :Java虚拟机无法解决的严重问题,比如JVM内部错误、资源耗尽等。一旦发生,程序无力回天。典型代表:
StackOverflowError(栈溢出)和OutOfMemoryError(内存溢出) -
Exception:程序员可以通过代码进行处理,使程序继续执行。我们平时所说的"异常"主要指的就是Exception
1.3 异常的分类
根据发生时机不同,异常分为两类:
编译时异常(受查异常)
在程序编译期间发生的异常,编译器会强制要求处理。比如:
public class Person implements Cloneable {
private String name;
@Override
public Person clone() {
return (Person) super.clone(); // 编译报错!未处理CloneNotSupportedException
}
}
运行时异常(非受查异常)
在程序执行期间发生的异常,编译器不强制处理。比如NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException等。
注意 :编译时出现的语法性错误不能称之为异常。比如把System.out.println写成了system.out.println,这是"编译错误",而不是异常。
二、异常的处理方式
2.1 防御式编程的两种思路
面对程序中不可避免的错误,主要有两种处理思路:
LBYL(事前防御型)
在操作之前就做充分的检查:
public static int getElement(int[] array, int index) {
if (array == null) {
System.out.println("数组为null");
return -1;
}
if (index < 0 || index >= array.length) {
System.out.println("下标越界");
return -1;
}
return array[index];
}
这种方式的缺陷是:正常流程和错误处理流程的代码混在一起,整体显得比较混乱。
EAFP(事后认错型)
先操作,遇到问题再处理:
public static int getElement(int[] array, int index) {
try {
return array[index];
} catch (NullPointerException e) {
System.out.println("数组为null");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("下标越界");
}
return -1;
}
这种方式将正常流程和错误流程分离开来,代码更清晰。Java异常处理的核心思想就是EAFP。
2.2 抛出异常:throw
当程序中出现错误时,可以使用throw关键字抛出一个异常对象,将错误信息告知调用者:
public static int getElement(int[] array, int index) {
if (null == array) {
throw new NullPointerException("传递的数组为null");
}
if (index < 0 || index >= array.length) {
throw new ArrayIndexOutOfBoundsException("传递的数组下标越界");
}
return array[index];
}
使用throw的注意事项:
-
throw必须写在方法体内部 -
抛出的对象必须是Exception或其子类对象
-
如果抛出的是RuntimeException或其子类,可以不处理,直接交给JVM
-
如果抛出的是编译时异常,必须处理,否则无法通过编译
-
异常一旦抛出,其后的代码就不会执行
2.3 声明异常:throws
throws关键字用于在方法声明中列出该方法可能抛出的异常,将异常的处理责任转移给调用者:
public void readConfig(String fileName) throws FileNotFoundException {
if (!fileName.equals("config.ini")) {
throw new FileNotFoundException("配置文件不是config.ini");
}
// 读取文件的逻辑...
}
使用throws的注意事项:
-
throws必须跟在方法的参数列表之后 -
声明的异常必须是Exception或其子类
-
如果方法内部可能抛出多个异常,
throws后跟多个异常类型,用逗号隔开 -
如果多个异常类型有父子关系,直接声明父类即可
-
调用声明抛出异常的方法时,如果该异常是编译时异常,调用者必须处理或继续抛出
2.4 捕获异常:try-catch
throws并没有真正处理异常,只是将异常上报给调用者。如果要真正处理异常,需要使用try-catch:
public static void main(String[] args) {
try {
readConfig("other.ini");
} catch (FileNotFoundException e) {
// 异常的处理方式
System.out.println(e.getMessage()); // 只打印异常信息
System.out.println(e); // 打印异常类型:异常信息
e.printStackTrace(); // 打印信息最全面(推荐)
}
// 异常被处理后,这里的代码会继续执行
System.out.println("异常如果被处理了,这里的代码也可以执行");
}
关于异常的处理方式:
异常种类繁多,要根据不同的业务场景来决定:
-
对于比较严重的问题(比如和金钱相关的场景),应该让程序直接崩溃,防止造成更严重后果
-
对于不太严重的问题(大多数场景),可以记录错误日志,并通过监控报警程序及时通知程序员
-
对于可能会恢复的问题(比如网络相关的场景),可以尝试重试
使用try-catch的注意事项:
-
try块内抛出异常位置之后的代码不会被执行 -
如果抛出异常类型与catch时异常类型不匹配,异常不会被成功捕获,继续往外抛
-
try中可能抛出多个不同的异常,需要用多个catch来捕获 -
如果多个异常的处理方式完全相同,可以写成:
catch(异常类型1 | 异常类型2 e) -
如果异常之间具有父子关系,子类异常必须在父类异常之前捕获
try {
System.out.println("before");
System.out.println(array[100]);
System.out.println("after");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数组下标越界异常");
e.printStackTrace();
} catch (NullPointerException e) {
System.out.println("空指针异常");
e.printStackTrace();
}
2.5 finally:无论是否异常都要执行
有些代码,不论程序是否发生异常,都需要执行。比如程序中打开的资源------网络连接、数据库连接、IO流等,在程序正常或异常退出时,必须进行回收。
finally就是用来解决这个问题的:
try {
// 可能会发生异常的代码
} catch (异常类型 e) {
// 对捕获到的异常进行处理
} finally {
// 此处的语句无论是否发生异常,都会被执行到
}
为什么需要finally?
考虑这样一个场景:方法中打开了输入流,在方法返回之前必须关闭它。如果不用finally,可能会出现资源泄漏:
public int getData() {
Scanner scanner = new Scanner(System.in);
try {
int num = scanner.nextInt();
return num;
} catch (InputMismatchException e) {
e.printStackTrace();
} finally {
scanner.close(); // 无论是否发生异常,都会执行
System.out.println("finally中代码");
}
return -1;
}
重要特性 :finally中的代码一定会在方法返回之前执行。但如果finally中也有return语句,它会覆盖try或catch中的return。因此,不建议在finally中写return语句。
2.6 异常的处理流程
当代码中出现异常时,会沿着"调用栈"向上传递:
-
程序先执行
try中的代码 -
如果
try中的代码出现异常,结束try中的代码,查看与catch中的异常类型是否匹配 -
如果找到匹配的异常类型,执行
catch中的代码 -
如果没有找到匹配的异常类型,将异常向上传递到上层调用者
-
无论是否找到匹配的异常类型,
finally中的代码都会被执行 -
如果上层调用者也没有处理异常,继续向上传递
-
一直到
main方法也没有处理,交给JVM处理,程序异常终止
三、自定义异常类
Java内置的异常类虽然丰富,但不能完全覆盖实际开发中的所有场景。比如实现一个用户登录功能,可能需要区分"用户名错误"和"密码错误"两种异常。
3.1 实现自定义异常
自定义异常通常继承自Exception或RuntimeException:
// 自定义异常:用户名错误
class UserNameException extends RuntimeException {
public UserNameException(String message) {
super(message);
}
}
// 自定义异常:密码错误
class PasswordException extends RuntimeException {
public PasswordException(String message) {
super(message);
}
}
然后在业务代码中使用:
public class LoginService {
private static final String USER_NAME = "admin";
private static final String PASSWORD = "123456";
public static void login(String userName, String password) {
if (!USER_NAME.equals(userName)) {
throw new UserNameException("用户名错误!");
}
if (!PASSWORD.equals(password)) {
throw new PasswordException("密码错误!");
}
System.out.println("登录成功!");
}
public static void main(String[] args) {
try {
login("admin", "654321");
} catch (UserNameException e) {
e.printStackTrace();
} catch (PasswordException e) {
e.printStackTrace();
}
}
}
3.2 注意事项
-
继承自
Exception的异常默认是受查异常(编译时异常),必须处理 -
继承自
RuntimeException的异常默认是非受查异常(运行时异常),可以不处理
四、课后练习
**题目1:** 以下哪个不是Java中的受检异常(checked exception)?
A. IOException
B. SQLException
C. NullPointerException
D. ClassNotFoundException
**答案:** C
**题目2:** 关于try-catch-finally语句,以下哪个说法是正确的?
A. finally块总是会执行,即使try块中有return语句
B. 如果catch块中抛出异常,finally块就不会执行
C. 一个try块后面必须跟着至少一个catch块
D. finally块中的return语句会覆盖try或catch块中的return语句
**答案:** A、D
**题目3:** 关于异常处理,以下哪个说法是错误的?
A. 子类方法抛出的异常范围不能大于父类方法声明的异常范围
B. 可以在catch块中捕获多个异常类型
C. RuntimeException及其子类都是非受查异常
D. 使用throw关键字可以手动抛出异常,但不能抛出Error
**答案:** D
总结
今天我们系统地学习了Java的异常处理机制:
-
异常的概念:程序执行过程中的不正常行为
-
异常体系:Throwable → Error(不可处理)和 Exception(可处理)
-
异常分类:编译时异常(受查异常)和运行时异常(非受查异常)
-
处理方式:throw(抛出)、throws(声明)、try-catch(捕获)、finally(收尾)
-
处理流程:沿调用栈向上传递,直到被处理或交给JVM
-
自定义异常:继承Exception或RuntimeException
异常处理不是可有可无的装饰,而是高质量软件的必备能力。一个好的程序不是不出错,而是在出错时能够优雅地处理,给用户友好的反馈,给开发者足够的信息来定位问题。
掌握异常处理,你的代码将更加健壮、更加专业。