从0开始学习Java+AI知识点总结-9.异常、泛型、集合框架
在 Java 中,泛型是一种 "参数化类型" 机制,允许在定义类、接口、方法时指定类型变量,从而在编译阶段约束数据类型,避免运行时类型转换异常。System.out.println("出现错误:" + e.getMessage());// 删除索引2的"B",返回"B"// 数组长度为3,索引最大为2,此处报错。// 在索引1插入"C",集合变为[A, C, B]// 等价于 int n = nu
作为 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 内置异常无法覆盖所有业务场景,此时可自定义异常类。
步骤:
- 继承RuntimeException(运行时异常)或Exception(编译时异常);
- 重写构造器(通常保留无参和带消息的构造器);
- 在业务逻辑中用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. 泛型的定义与使用
泛型可用于类、接口、方法,核心是通过类型变量(如E、T、K、V)表示未知类型。
(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. 泛型与包装类
泛型不支持基本数据类型(如int、double),需使用对应的包装类(引用类型)。
基本类型与包装类对应关系:
基本类型 |
包装类 |
基本类型 |
包装类 |
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 集合的核心特点是有序(存序 = 取序)、可重复、有索引,常用实现类为ArrayList和LinkedList。
(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。
解决方案:
- 使用普通 for 循环(通过索引遍历,增删后调整索引);
- 使用迭代器的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...catch和throws;
- 泛型通过类型约束保证类型安全,避免强制转换异常;
- 集合框架提供灵活的数据存储方案,List 适合有序场景,ArrayList 和 LinkedList 各有侧重。
这些知识点是 Java 基础的重中之重,建议初学者结合实例多练习,理解原理后再深入框架源码。后续会继续讲解 Map 集合、集合工具类等内容,敬请关注!
如果觉得本文有帮助,欢迎点赞、收藏,也欢迎在评论区交流学习心得~
更多推荐
所有评论(0)