一、Java 异常 (Exception) 秒懂:程序出错时的“急救手册”
写代码就像开车上班。理想情况下,一路绿灯顺利到达(程序正常运行完毕)。
但现实中,可能会遇到车胎扎钉子了、前方修路绕行了,甚至发动机直接报废了。在 Java 中,这些导致程序无法正常运行的突发事件,统称为异常(Exception)。
今天我们就来彻底搞懂 Java 的异常体系,以及如何优雅地处理它们。
1. 异常的终极族谱
在 Java 中,所有的异常和错误都有一个共同的老祖宗:Throwable。它有两个性格迥异的儿子:
-
Error(错误):这是极其严重的系统级问题,比如内存溢出(OutOfMemoryError)、栈溢出。- 生活比喻:开车遇到地震、高架桥塌了。这种问题你作为驾驶员(程序员)根本没法处理,只能弃车逃生(程序直接崩溃)。
-
Exception(异常):这是程序级别的问题,是我们今天讨论的主角。程序可以通过修改代码来预判并挽救它。
Exception 下面又分为两大阵营:
① 运行时异常 (RuntimeException / Unchecked)
- 特点:在写代码和编译阶段,编译器不会强制要求你处理它。只有等代码真正跑起来时,才会突然报错。
-
常见代表:
-
NullPointerException(空指针异常:最常见的杀手) -
IndexOutOfBoundsException(数组越界异常) -
ArithmeticException(算术异常,比如除以 0)
-
-
生活比喻:没油了、方向盘打错了。这往往是驾驶员(程序员)的逻辑失误导致的,本应该在写代码时用
if判断来避免。
② 编译时异常 (Checked Exception)
-
特点:除了
RuntimeException及其子类,其他的异常都属于编译时异常。编译器会死死盯着你,强迫你必须在写代码时就给出解决方案,否则连编译都过不去(代码直接飘红)。 -
常见代表:
IOException(文件读写异常)、SQLException(数据库异常)、ParseException(解析异常)。 - 生活比喻:出门遇到暴雨、前方突然堵车。这是外界环境导致的客观问题,交规(编译器)强制要求你出门前必须带伞或想好备用路线。
2. 处理异常的两大绝招
当代码抛出异常时,我们该怎么办?Java 给我们提供了两套截然不同的应对方案。
绝招一:throws —— 甩锅大法
当我在方法里遇到异常,但我不知道怎么处理,或者我不想处理时,我可以在方法签名上加上 throws,把异常抛给调用我的那个人。
// 方法 A 表示:我可能会抛出文件找不到的异常,谁调用我,谁负责解决!
public void readFile() throws FileNotFoundException {
FileInputStream fis = new FileInputStream("D:/不存在的文件.txt");
}
注意:甩锅不能无限甩。如果一路甩锅到了 main 方法,main 方法还甩给 JVM,那 JVM 的处理方式就是:直接打印红字报错,然后粗暴地终止程序。
绝招二:try-catch —— 自己兜底
作为一个负责任的程序员,我们通常会选择把可能出错的代码包起来,一旦出错,自己内部消化处理,不让程序崩溃。
public void readFile() {
try {
// 【监视区】把可能会发生异常的代码放在这里
FileInputStream fis = new FileInputStream("D:/不存在的文件.txt");
System.out.println("文件读取成功"); // 如果上面报错,这行代码不会执行
} catch (FileNotFoundException e) {
// 【急救区】一旦 try 里面抓到了 FileNotFoundException,就会跳到这里执行
System.out.println("别慌,文件没找到,我给用户弹个提示框就好了");
// 打印详细的异常追踪信息(开发阶段必写)
e.printStackTrace();
} finally {
// 【善后区】(可选) 无论是否发生异常,最后绝对会执行的代码!
// 通常用来释放资源,比如关闭文件流、关闭数据库连接
System.out.println("这里的代码是一定会执行的");
}
}
3. 高阶玩法:自定义异常
虽然 Java 自带了很多异常,但在实际开发中,我们往往需要针对自己的业务定义专有的异常,比如“年龄越界异常”、“余额不足异常”。
自定义异常非常简单,只需要两步:
- 写一个类继承
Exception(强迫使用者 try-catch)或继承RuntimeException(不强迫使用者处理,企业中最常用)。 - 提供几个构造器。
// 1. 继承 RuntimeException
public class AgeOutOfBoundsException extends RuntimeException {
// 2. 提供无参构造
public AgeOutOfBoundsException() {
}
// 3. 提供带详细信息的有参构造
public AgeOutOfBoundsException(String message) {
super(message);
}
}
// 怎么使用它?
public void setAge(int age) {
if (age < 0 || age > 150) {
// 业务逻辑不符合时,主动使用 throw 抛出我们自定义的异常对象!
throw new AgeOutOfBoundsException("年龄输入不合法:" + age);
}
this.age = age;
}
总结
-
遇到编译时异常:老老实实写
try-catch处理,或者用throws甩给上层。 -
遇到运行时异常:多半是你代码逻辑没写严谨,赶紧去加
if-else防御,或者排查空指针! -
黄金法则:在企业开发中,尽量做到“早抛出,晚捕获”(在底层及早发现问题
throw出去,在最外层的 Controller 或统一异常处理器中集中catch并返回给前端友好的提示)。
二、Java 泛型 (Generics) 秒懂:给你的代码贴上“专属标签”
在 JDK 1.5 之前,Java 是没有泛型的。那时候把对象丢进集合(比如 ArrayList)就像把东西扔进一个“无底洞垃圾桶”,所有东西进去后都会被当成最宽泛的 Object 类型。
拿出来的时候,你必须进行危险的强制类型转换,稍微记错类型,程序就会在运行时报 ClassCastException 而崩溃。
为了解决这个痛点,Java 引入了泛型(Generics)。它的本质是参数化类型,就像是给收纳盒贴上了一个“专属标签”:如果贴了 <String> 的标签,编译器就会死死盯住,绝对不准你往里面扔数字!
1. 泛型类:打造一个万能收纳盒
如果一个类在定义时,在类名后面加上了 <E> 或 <T> 这样的泛型标识,它就是泛型类。
- 使用场景:当一个类的某个属性,或者某个方法的参数/返回值,其类型目前还不确定,要等别人用这个类的时候再决定时。
- 实战代码:
// 这里的 <T> 是一个占位符,代表某种未知的类型 (Type)
public class Box<T> {
private T content; // 属性类型是 T
public void put(T content) {
this.content = content;
}
public T get() {
return content;
}
}
// 怎么用?在使用时贴上具体的标签!
Box<String> stringBox = new Box<>();
stringBox.put("黑马程序员"); // 只能放字符串,放别的直接编译报错!
Box<Integer> intBox = new Box<>();
intBox.put(1024); // 现在这个盒子变成了专属的数字盒
2. 泛型接口:灵活的契约规则
和泛型类极其相似,接口也可以定义泛型,最经典的就是 Java 源码里的 List<E> 接口。
-
定义格式:
public interface Data<E> { void add(E e); } -
实现规则:实现泛型接口时,有两种选择。
-
直接把类型定死:
class StringData implements Data<String> -
继续做个“渣男”,把泛型传递下去:
class MyData<E> implements Data<E>
-
直接把类型定死:
3. 泛型方法与神仙通配符 (?)
① 泛型方法
有时候我们不想让整个类都变成泛型类,仅仅只是某个方法需要接收任意类型的参数,这时就可以使用泛型方法。
-
铁律:泛型的声明
<T>必须写在方法的修饰符之后,返回值类型之前!
// 这是一个泛型方法,可以接收任意类型的数组并打印
public static <T> void printArray(T[] arr) {
for (T t : arr) {
System.out.println(t);
}
}
② 通配符 (?) 与上下限
在开发中,泛型是没有“多态”的!也就是说,ArrayList<Animal> 和 ArrayList<Dog> 是两种完全不兼容的类型。
如果我们写了一个方法,既想接收装狗的集合,又想接收装猫的集合,怎么办?使用通配符 ?。
-
?(无界通配符):代表任意类型。比如ArrayList<?>可以接收任何集合,但缺点是它只能读取,不能往里添加元素(因为编译器不知道里面到底装的是啥)。 -
? extends 父类(上限通配符):代表只能是该类或其子类。-
场景:
ArrayList<? extends Animal>,表示这个集合里装的必须是动物家族的成员。
-
场景:
-
? super 子类(下限通配符):代表只能是该类或其父类。
4. 新手巨坑:泛型支持的类型
这是一个无数新手都会踩坑的绝对禁区:
⚠️ 泛型不支持基本数据类型,它只能支持引用数据类型!
// ❌ 灾难错误:编译直接不通过!
ArrayList<int> list1 = new ArrayList<>();
ArrayList<double> list2 = new ArrayList<>();
// ✅ 正确做法:必须使用基本数据类型对应的“包装类”!
ArrayList<Integer> list1 = new ArrayList<>();
ArrayList<Double> list2 = new ArrayList<>();
总结
- 本质:把类型明确的工作推迟到创建对象或调用方法时(参数化类型)。
-
最大优势:将运行时的类型转换报错(
ClassCastException),提前到了编译期,让错误在写代码时就原形毕露。 -
口诀:泛型类看实例化,泛型方法看调用,遇到泛型集合传参,认准通配符
?。