异常、泛型

一、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 自带了很多异常,但在实际开发中,我们往往需要针对自己的业务定义专有的异常,比如“年龄越界异常”、“余额不足异常”。

自定义异常非常简单,只需要两步:

  1. 写一个类继承 Exception(强迫使用者 try-catch)或继承 RuntimeException(不强迫使用者处理,企业中最常用)。
  2. 提供几个构造器。
// 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); }
  • 实现规则:实现泛型接口时,有两种选择。
    1. 直接把类型定死class StringData implements Data<String>
    2. 继续做个“渣男”,把泛型传递下去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),提前到了编译期,让错误在写代码时就原形毕露。
  • 口诀:泛型类看实例化,泛型方法看调用,遇到泛型集合传参,认准通配符 ?
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容