作为 Java 初学者,异常处理、泛型和集合框架是绕不开的核心知识点。这些内容不仅是日常开发的基础,也是面试高频考点。本文将用通俗易懂的语言,结合实例详细讲解这些知识点,帮助快速掌握。

一、异常处理:程序问题的 "预警系统"

在程序运行过程中,难免会出现各种意外情况(比如数组越界、文件找不到),这些情况在 Java 中被统一称为异常。学会处理异常,能让程序更健壮、更易维护。

1. 什么是异常?

异常本质上是程序运行时出现的错误,Java 会将这些错误封装成异常对象,包含错误信息和发生位置。

举个简单例子:

// 数组索引越界异常(运行时异常)

int[] arr = {10, 20, 30};

System.out.println(arr[3]); // 数组长度为3,索引最大为2,此处报错

// 算术异常(运行时异常)

System.out.println(10 / 0); // 除数不能为0,此处报错

2. 异常体系结构

Java 中所有异常都继承自Throwable类,主要分为两类:

  • Error(错误):系统级严重问题(如内存溢出),由 JVM 生成,开发者无需处理,程序通常会直接崩溃。
  • Exception(异常):程序可处理的问题,分为两类:
    • 运行时异常(RuntimeException 及其子类):编译时不报错,运行时才出现(如数组越界、空指针)。
    • 编译时异常:编译阶段就必须处理的异常(如文件不存在、网络连接失败),否则程序无法通过编译。

(示意图)

3. 异常的处理方案

遇到异常时,Java 提供了两种核心处理方式:

(1)抛出异常(throws)

在方法上使用throws关键字,将异常抛给上层调用者处理,自己不直接解决。

语法:

// 声明方法可能抛出的异常

public void readFile() throws FileNotFoundException {

    // 读取文件的代码(可能抛出FileNotFoundException)

}

(2)捕获异常(try...catch)

使用try块监视可能出错的代码,用catch块捕获并处理异常。

语法:

try {

    // 可能出现异常的代码

    int result = 10 / 0;

} catch (ArithmeticException e) {

    // 处理异常:打印信息或自定义逻辑

    System.out.println("出现错误:" + e.getMessage()); // 输出:出现错误:/byzero

}

注意:多个catch块捕获异常时,需按 "从小到大" 的顺序(子类在前,父类在后),最后可加Exception捕获所有异常。

4. 自定义异常

Java 内置异常无法覆盖所有业务场景,此时可自定义异常类。

步骤:

  1. 继承RuntimeException(运行时异常)或Exception(编译时异常);
  2. 重写构造器(通常保留无参和带消息的构造器);
  3. 在业务逻辑中用throw关键字抛出。

示例:

// 自定义编译时异常

public class AgeException extends Exception {

    // 无参构造器

    public AgeException() {}

    // 带错误消息的构造器

    public AgeException(String message) {

        super(message);

    }

}

// 使用自定义异常

public void checkAge(int age) throws AgeException {

    if (age < 0 || age > 150) {

        throw new AgeException("年龄必须在0-150之间"); // 抛出异常

    }

}

异常小结

  • 异常是程序运行中的错误,分为 Error(无需处理)和 Exception(需处理);
  • 处理异常的核心是throws(抛出)和try...catch(捕获);
  • 自定义异常可满足业务特殊需求,提高代码可读性。

二、泛型:类型安全的 "守护者"

在 Java 中,泛型是一种 "参数化类型" 机制,允许在定义类、接口、方法时指定类型变量,从而在编译阶段约束数据类型,避免运行时类型转换异常。

1. 为什么需要泛型?

没有泛型时,集合中可以存储任意类型数据,取出时需强制转换,容易出错:

// 无泛型的问题

List list = new ArrayList();

list.add("Java");

list.add(123); // 可以添加整数

String str = (String) list.get(1); // 运行时报错:Integer不能转String

有了泛型后,编译时就会检查类型,避免错误:

// 有泛型的安全用法

List<String> list = new ArrayList<>();

list.add("Java");

// list.add(123); // 编译直接报错:类型不匹配

String str = list.get(0); // 无需强制转换

2. 泛型的定义与使用

泛型可用于类、接口、方法,核心是通过类型变量(如ETKV)表示未知类型。

(1)泛型类

定义类时声明类型变量,在类中可作为属性、方法参数或返回值类型。

语法:

// 泛型类定义:T为类型变量

public class Box<T> {

    private T value; // 使用泛型作为属性类型

    // 泛型作为方法参数和返回值

    public void setValue(T value) {

        this.value = value;

    }

    public T getValue() {

        return value;

    }

}

// 使用泛型类

Box<String> stringBox = new Box<>();

stringBox.setValue("Hello");

String str = stringBox.getValue(); // 类型安全

Box<Integer> intBox = new Box<>();

intBox.setValue(123);

int num = intBox.getValue();

(2)泛型接口

接口中声明类型变量,实现类需指定具体类型或继续保留泛型。

语法:

// 泛型接口定义

public interface DataDao<T> {

    void save(T data);

    T findById(int id);

}

// 实现接口时指定具体类型

public class UserDao implements DataDao<User> {

    @Override

    public void save(User data) { /* 实现 */ }

    @Override

    public User findById(int id) { /* 实现 */ }

}

(3)泛型方法

方法中声明类型变量,可独立于类的泛型使用。

语法:

public class GenericMethodDemo {

    // 泛型方法:<T>为方法声明的类型变量

    public static <T> void print(T data) {

        System.out.println(data);

    }

    // 调用泛型方法

    public static void main(String[] args) {

        print("Java"); // T自动推断为String

        print(123);    // T自动推断为Integer

    }

}

3. 泛型通配符与上下限

当需要限制泛型的范围时,可使用通配符?和上下限:

  • 通配符?:表示任意类型(仅用于使用泛型时,而非定义时)。
  • 上限? extends 类型:只能接收该类型及其子类。
  • 下限? super 类型:只能接收该类型及其父类。

示例:

// 上限:只能传入Number或其子类(如Integer、Double)

public void printNumberList(List<? extends Number> list) {

    for (Number num : list) {

        System.out.println(num);

    }

}

// 下限:只能传入Integer或其父类(如Number、Object)

public void addInteger(List<? super Integer> list) {

    list.add(123); // 安全添加Integer

}

4. 泛型与包装类

泛型不支持基本数据类型(如intdouble),需使用对应的包装类(引用类型)。

基本类型与包装类对应关系:

基本类型

包装类

基本类型

包装类

byte

Byte

float

Float

short

Short

double

Double

int

Integer

boolean

Boolean

long

Long

char

Character

自动装箱与拆箱:基本类型与包装类可自动转换:

// 自动装箱:int -> Integer

Integer num = 123; // 等价于 Integer num = Integer.valueOf(123);

// 自动拆箱:Integer -> int

int n = num; // 等价于 int n = num.intValue();

包装类常用功能:

  • 类型转换:Integer.parseInt("123")(字符串转 int)、Double.toString(3.14)(数字转字符串)。
  • 常量定义:Integer.MAX_VALUE(int 最大值)、Double.MIN_VALUE(double 最小值)。

泛型小结

  • 泛型通过类型变量约束数据类型,避免强制转换和运行时异常;
  • 泛型可用于类、接口、方法,提高代码复用性;
  • 通配符上下限用于限制泛型范围,包装类解决泛型不支持基本类型的问题。

三、集合框架:数据存储的 "万能容器"

集合是 Java 中用于存储多个数据的容器,相比数组,集合大小可变、支持多种数据结构,是开发中最常用的工具之一。

1. 集合体系结构

Java 集合框架主要分为两大体系:

  • Collection:单列集合,每个元素是单个值(如 "苹果"、123);
  • Map:双列集合,每个元素是键值对(如"name": "Java")。

本文重点讲解 Collection 体系,其核心结构如下:

Collection

├─ List(有序、可重复、有索引)

│  ├─ ArrayList(底层数组,查询快)

│  └─ LinkedList(底层双链表,增删快)

└─ Set(无序、不重复、无索引)

   ├─ HashSet(无序)

   ├─ LinkedHashSet(有序)

   └─ TreeSet(排序)

2. Collection 常用功能

Collection 是所有单列集合的父接口,定义了通用方法,所有子类都可使用:

方法名

说明

add(E e)

添加元素,成功返回 true

remove(E e)

删除指定元素,成功返回 true

contains(Object obj)

判断是否包含指定元素

size()

返回元素个数

isEmpty()

判断集合是否为空

clear()

清空集合所有元素

toArray()

转换为 Object 数组

示例:

Collection<String> coll = new ArrayList<>();

coll.add("Java");

coll.add("Python");

System.out.println(coll.size()); // 输出:2

System.out.println(coll.contains("Java")); // 输出:true

coll.remove("Python");

coll.clear();

3. Collection 遍历方式

遍历集合有三种常用方式,适用于所有 Collection 实现类:

(1)迭代器遍历

迭代器是集合专用遍历工具,通过hasNext()判断是否有下一个元素,next()获取元素。

Collection<String> coll = new ArrayList<>();

coll.add("A");

coll.add("B");

// 获取迭代器

Iterator<String> it = coll.iterator();

// 遍历

while (it.hasNext()) {

    String element = it.next(); // 获取元素并移动指针

    System.out.println(element);

}

注意next()需在hasNext()为 true 时调用,否则会抛出NoSuchElementException

(2)增强 for 循环(foreach)

简化版迭代器,语法更简洁,适合仅遍历不修改的场景。

for (String element : coll) { // 变量element依次接收集合元素

    System.out.println(element);

}

(3)Lambda 表达式(JDK 8+)

通过forEach()方法结合 Lambda 表达式,代码更简洁。

coll.forEach(element -> System.out.println(element));

// 简化写法

coll.forEach(System.out::println);

4. List 集合:有序可重复的 "数组增强版"

List 集合的核心特点是有序(存序 = 取序)、可重复、有索引,常用实现类为ArrayListLinkedList

(1)List 特有功能

由于支持索引,List 新增了一批索引相关方法:

方法名

说明

add(int index, E e)

在指定索引插入元素

remove(int index)

删除指定索引元素,返回被删除元素

set(int index, E e)

修改指定索引元素,返回旧元素

get(int index)

返回指定索引元素

示例:

List<String> list = new ArrayList<>();

list.add("A");

list.add("B");

list.add(1, "C"); // 在索引1插入"C",集合变为[A, C, B]

String removed = list.remove(2); // 删除索引2的"B",返回"B"

list.set(0, "D"); // 修改索引0为"D",集合变为[D, C]

String element = list.get(1); // 获取索引1的"C"

(2)ArrayList:查询为王

ArrayList底层基于动态数组实现,特点:

  • 查询快:通过索引直接定位元素(如array[2]);
  • 增删慢:中间增删需移动大量元素(如在数组中间插入元素,后续元素需后移)。

适用场景:频繁查询、少量增删的场景(如数据展示列表)。

(3)LinkedList:增删能手

LinkedList底层基于双链表实现(每个元素包含前后指针),特点:

  • 增删快:只需修改前后元素的指针,无需移动大量元素;
  • 查询慢:需从头 / 尾遍历到目标索引;
  • 首尾操作极快:直接操作头节点和尾节点。

LinkedList特有首尾操作方法:

方法名

说明

addFirst(E e)

在首部添加元素

addLast(E e)

在尾部添加元素

getFirst()

获取首部元素

getLast()

获取尾部元素

removeFirst()

删除并返回首部元素

removeLast()

删除并返回尾部元素

适用场景:频繁增删、首尾操作的场景(如队列、栈)。

(4)并发修改异常

遍历集合时同时增删元素,可能触发ConcurrentModificationException

解决方案:

  1. 使用普通 for 循环(通过索引遍历,增删后调整索引);
  2. 使用迭代器的remove()方法(仅迭代器自身修改集合)。

示例(正确删除方式):

// 方案1:普通for循环

for (int i = 0; i < list.size(); i++) {

    if (list.get(i).contains("枸杞")) {

        list.remove(i);

        i--; // 删除后索引回退,避免漏查

    }

}

// 方案2:迭代器删除

Iterator<String> it = list.iterator();

while (it.hasNext()) {

    if (it.next().contains("枸杞")) {

        it.remove(); // 迭代器自身删除,安全

    }

}

集合框架小结

  • 集合是动态存储容器,分为 Collection(单列)和 Map(双列);
  • List 有序可重复有索引,ArrayList 适合查询,LinkedList 适合增删;
  • 遍历集合需注意并发修改异常,优先用迭代器或普通 for 循环处理增删。

总结

本文详细讲解了 Java 异常、泛型和集合框架三大核心知识点:

  • 异常处理帮助程序优雅应对错误,核心是try...catchthrows
  • 泛型通过类型约束保证类型安全,避免强制转换异常;
  • 集合框架提供灵活的数据存储方案,List 适合有序场景,ArrayList 和 LinkedList 各有侧重。

这些知识点是 Java 基础的重中之重,建议初学者结合实例多练习,理解原理后再深入框架源码。后续会继续讲解 Map 集合、集合工具类等内容,敬请关注!

如果觉得本文有帮助,欢迎点赞、收藏,也欢迎在评论区交流学习心得~

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐