
Java核心与AI开发进阶
第十二章:集合高级
集合是长度可变的容器。Java 集合按”单列/双列”分为两大体系:单列(一次添加一个元素,对应
Collection接口)和双列(一次添加两个元素即键值对,对应Map接口)。本篇覆盖Collection、List、常用数据结构、ArrayList/LinkedList以及泛型基础。
一、集合体系结构
整体划分:
| 分类 | 顶层接口 | 子接口 / 实现类 | 一次添加 | 特点 |
|---|---|---|---|---|
| 单列集合 | Collection | List:ArrayList、LinkedList | 1 个元素 | 有序 |
| 单列集合 | Collection | Set:TreeSet、HashSet、LinkedHashSet | 1 个元素 | 无序 |
| 双列集合 | Map | TreeMap、HashMap、LinkedHashMap | 2 个元素(键值对) | 键不可重复 |
List 与 Set 的核心区别:
| 接口 | 存取 | 索引 | 重复元素 |
|---|---|---|---|
| List 接口 | 存取有序 | 有 | 可以存储重复 |
| Set 接口 | 存取无序 | 没有 | 不可以存储重复 |
注意:单列集合调用
add(元素),双列集合调用put(键, 值)。
二、Collection 接口的使用
Collection是所有单列集合的父接口,其内部方法(add/remove/contains/ …)所有单列集合都可以使用。
| 方法 | 说明 |
|---|---|
public boolean add(E e) | 把给定的对象添加到当前集合中 |
public void clear() | 清空集合中所有的元素 |
public boolean remove(E e) | 把给定的对象在当前集合中删除 |
public boolean contains(Object obj) | 判断当前集合中是否包含给定的对象 |
public boolean isEmpty() | 判断当前集合是否为空 |
public int size() | 返回集合中元素的个数(集合的长度) |
⚠ 关键点:
remove()与contains()方法底层依赖对象的equals方法判断是否相等。若集合中存的是自定义对象(如Student),未重写equals时会调用Object.equals(比地址),需重写才能按内容比较。
三、集合的通用遍历方式
通用遍历方式有三种,
Set没有索引不能用普通 for 循环,所以后两种才真正”通用”。
| 方式 | 适用场景 | 备注 |
|---|---|---|
| 普通 for 循环 | 仅 List(需索引) | 写法 list.get(i),Set 不可用 |
| 迭代器 Iterator | 所有 Collection | hasNext() 判断 + next() 取值并后移 |
| 增强 for 循环 | 所有 Collection 与数组 | JDK5 引入,内部原理就是 Iterator |
普通 for(仅 List):
ArrayList<String> list = new ArrayList<>();for (int i = 0; i < list.size(); i++) { String s = list.get(i);}迭代器:
Iterator<String> it = list.iterator();while (it.hasNext()) { String s = it.next(); System.out.println(s);}注意:
next()方法会取出当前元素并将指针向后移动一位。调用次数不能超过hasNext()的次数,否则抛NoSuchElementException。
增强 for(语法糖):
for (String s : list) { // 内部就是 Iterator System.out.println(s);}四、List 接口
List因为支持索引,所以在Collection基础上多了一组”索引操作”的独特 API。
| 方法 | 说明 |
|---|---|
void add(int index, E element) | 在集合中的指定位置插入指定元素 |
E remove(int index) | 删除指定索引处的元素,返回被删除的元素 |
E set(int index, E element) | 修改指定索引处的元素,返回被修改的元素 |
E get(int index) | 返回指定索引处的元素 |
List 接口的 5 种遍历方式:迭代器 Iterator、增强 for、foreach 方法、普通 for 循环、列表迭代器 ListIterator。
4.1 并发修改异常
使用迭代器遍历集合的过程中,调用了集合对象的
add/remove方法,就会出现ConcurrentModificationException(并发修改异常)。
| 想要的操作 | 正确做法 |
|---|---|
| 迭代过程中做删除 | 使用 Iterator 自带的 remove 方法 |
| 迭代过程中做添加 | 使用 ListIterator 自带的 add 方法 |
// 错误写法:会抛 ConcurrentModificationExceptionIterator<String> it = list.iterator();while (it.hasNext()) { String s = it.next(); if ("xxx".equals(s)) { list.remove(s); // ❌ }}
// 正确写法:用迭代器自己的 remove()Iterator<String> it = list.iterator();while (it.hasNext()) { String s = it.next(); if ("xxx".equals(s)) { it.remove(); // ✅ }}五、数据结构
集合的”底层”就是数据结构:
ArrayList用数组,LinkedList用链表,Stack/Queue用栈 / 队列。理解数据结构才能选对集合。
5.1 栈(Stack)
栈是一种 先进后出(后进先出) 的线性表,像弹匣——只有一端开口(栈顶),另一端封闭(栈底)。
| 操作 | 别名 | 方向 |
|---|---|---|
| 入栈 | 压栈 / push | 从栈顶进入 |
| 出栈 | 弹栈 / pop | 从栈顶出去 |
flowchart TB A["数据A"] -->|压栈| B["数据A 数据B"] B -->|压栈| C["数据A 数据B 数据C"] C -->|弹栈| B B -->|弹栈| A
5.2 队列(Queue)
队列是 先进先出 的线性表,像排队——两端都开口。
| 位置 | 操作 | 方向 |
|---|---|---|
| 后端(rear) | 入队 / offer | 元素从后端进入 |
| 前端(front) | 出队 / poll | 元素从前端出去 |
5.3 数组(Array)
数组在内存中连续存储,每个元素有固定下标(索引)。
| 优点 | 缺点 |
|---|---|
| 查询速度快:通过地址值 + 索引定位,查询任意数据耗时相同 | 增删效率低:新增 / 删除数据时,可能大批量移动数组中其他元素 |
5.4 链表(Linked List)
链表中的结点是独立的内存块,结点之间通过”下一个结点地址”串联,内存不连续。
| 优点 | 缺点 |
|---|---|
| 增删相对快:只需要改变相邻结点的地址指向 | 查询慢:无论查哪个数据都要从头开始找 |
单向链表 vs 双向链表:
| 类型 | 存储内容 | 遍历方向 |
|---|---|---|
| 单向链表 | 数据 + 下一个结点地址 | 只能从头到尾 |
| 双向链表 | 前一个结点地址 + 数据 + 下一个结点地址 | 可前后双向,查找第 N 个元素更灵活 |
链表的插入过程(在数据 A、C 之间插入 B):
- 数据 B 对应的下一个数据地址指向数据 C
- 数据 A 对应的下一个数据地址指向数据 B
六、ArrayList 类
ArrayList底层是数组实现的,因此根据索引查询元素快,增删相对慢。
6.1 长度可变原理
flowchart LR S1["1. 底层创建 长度 10 的空数组"] --> S2["2. 存满后扩容 原数组 × 1.5"] S2 --> S3["3. 拷贝原数据 到新数组"] S3 --> S4["4. 添加新元素 到新数组"]
四步概括:① 初始长度 10 的空数组 → ② 存满后扩容 1.5 倍 → ③ 旧数据拷贝到新数组 → ④ 新元素添加到新数组。
6.2 源码解析
| 步骤 | 行为 |
|---|---|
| ① | 使用空参构造器创建的集合,底层会创建一个默认长度为 0 的数组 |
| ② | 添加第一个元素时,底层会创建一个新的长度为 10 的数组 |
| ③ | 数组存满时,会扩容 1.5 倍 |
6.3 LinkedList 类
LinkedList底层基于双链表实现:查询元素慢,增删首尾元素非常快。
| 特有方法 | 说明 |
|---|---|
public void addFirst(E e) | 在该列表开头插入指定的元素 |
public void addLast(E e) | 将指定的元素追加到此列表的末尾 |
public E getFirst() | 返回此列表中的第一个元素 |
public E getLast() | 返回此列表中的最后一个元素 |
public E removeFirst() | 从此列表中删除并返回第一个元素 |
public E removeLast() | 从此列表中删除并返回最后一个元素 |
思考:
LinkedList也有get(int index)方法,表面看起来是根据索引获取元素,实际上要从头 / 尾开始逐个查找,所以查询性能差;如果大量查询请用ArrayList,大量首尾增删请用LinkedList。
七、泛型(Generics)
JDK5 引入的特性,可以在编译阶段约束操作的数据类型,并进行检查。
7.1 泛型的好处
- 统一数据类型
- 将运行期的错误提升到编译期(编译时就能发现类型不匹配)
⚠ 注意事项:泛型中只能编写引用数据类型,不能写基本类型(要用包装类:
int→Integer、double→Double)。
// ❌ 错误ArrayList<int> list = new ArrayList<>();
// ✅ 正确ArrayList<Integer> list = new ArrayList<>();7.2 泛型类
在类名后用
<E>声明类型占位符,类中即可用E表示”某种类型”。
public class ArrayList<E> { public boolean add(E e) { ... }}7.3 泛型方法
| 场景 | 泛型声明方式 |
|---|---|
| 非静态方法 | 泛型根据类的泛型去匹配 |
| 静态方法 | 需要自己声明独立的泛型(静态先于对象存在,无法用类的泛型) |
// 非静态:使用类上声明的 Epublic class ArrayList<E> { public boolean add(E e) { ... }}
// 静态:必须自己声明 <T>public static <T> void printArray(T[] array) { ... }7.4 泛型接口
类实现带泛型的接口时,有两种操作方式:
- 实现接口时直接确定类型:
public class ArrayList<E> implements List<E> - 保持接口的泛型,等创建对象时再确定:
public class ArrayList<E> implements List<E>
7.5 泛型通配符 ?
用于方法形参接收”任意类型”的集合,常配合上下限
? extends T/? super T使用(进阶内容,留到后续章节)。
集合高级(2)
本篇聚焦
Set家族(TreeSet/HashSet/LinkedHashSet)和Map家族(HashMap/LinkedHashMap/TreeMap)的实现原理、API 与遍历方式,以及Collections工具类的常用方法(含可变参数)。
一、Set 集合介绍
Set接口是Collection的子接口。Set实现类有 3 个:TreeSet/HashSet/LinkedHashSet,整体特点是:存取顺序不一致,没有跟索引有关的方法,不能存储重复元素。
| 实现类 | 特点 | 底层 |
|---|---|---|
| TreeSet | 元素排序 | 红黑树 |
| HashSet | 元素唯一(不保证顺序) | 哈希表 |
| LinkedHashSet | 元素唯一,保证存取顺序 | 哈希表 + 双向链表 |
注意:
Set没有索引,三种通用遍历方式(迭代器 / 增强 for / foreach)都可用,普通 for 循环不可用。
二、TreeSet 集合
TreeSet特点是元素按规则排序(默认自然排序 / 自定义比较器),本质底层是红黑树(一种自平衡的二叉查找树)。
2.1 底层数据结构演进
| 数据结构 | 规则 | 缺点 |
|---|---|---|
| 普通的二叉树 | 父结点只跟子结点相连 | 退化成链表时查询效率 O(n) |
| 二叉查找树(二叉排序树) | 小的存左边,大的存右边,一样的不存 | 数据倾斜时仍可能退化成链表 |
| 平衡二叉树 | 在满足查找二叉树规则下,让树尽可能矮小,提高查询性能 | 频繁旋转开销大 |
| 红黑树(TreeSet 实际用) | 近似平衡的二叉查找树,规则更灵活,旋转更少 | — |
2.2 两种排序方式
TreeSet支持两种排序方式:自然排序(元素自身实现Comparable)和比较器排序(构造时传入Comparator)。
自然排序三步骤:
- 类实现
Comparable接口 - 重写
compareTo方法 - 根据
compareTo返回值组织排序规则:负数:左边走、正数:右边走、0:不存
public class Student implements Comparable<Student> { @Override public int compareTo(Student o) { return this.age - o.age; // 按年龄升序 }}注意:
TreeSet取出的顺序是左中右(中序遍历),张三, 23 李四, 24 王五, 25 赵六, 26。
比较器排序:当同具备自然排序和比较器排序时,会优先按照比较器进行排序操作。当 String / Integer / Double 等内置自然排序规则不符合需求时(如 String 是字典序,需要按长度排),使用比较器排序。
三、HashSet 集合
HashSet特点:元素唯一、不保证顺序。底层是哈希表,因此存取速度快。
3.1 hashCode 与 equals 配合流程
哈希值是一个
int类型的随机值,Java 中每个对象都有一个哈希值(由Object.hashCode()底层 C++ 计算)。自定义对象必须按”内容”实现hashCode+equals,否则重复元素识别不出来。
hashCode 的作用:定位元素应存入哪个桶(数组下标);equals 的作用:同一桶内的元素去重判断。
@Overridepublic boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return age == student.age && Objects.equals(name, student.name);}
@Overridepublic int hashCode() { return Objects.hash(name, age);}⚠ 关键点:根据对象内容生成 hash 值,不同的对象 hash 值也可能相等(如
"重地"和"通话"),这种现象叫 哈希碰撞。
3.2 HashSet 底层原理
HashSet底层就是HashMap,存储流程基于哈希表(JDK 8 前后结构不同)。
| 版本 | 底层结构 |
|---|---|
| JDK 8 之前 | 数组 + 链表 |
| JDK 8 之后 | 数组 + 链表 + 红黑树 |
哈希表存储数据的详细流程:
flowchart TD
S1["① 创建默认长度16的数组(加载因子 0.75)"] --> S2["② 用元素的哈希值跟数组长度 计算应存入的位置"]
S2 --> S3{"③ 该位置是否为 null?"}
S3 -->|是 null| S4["直接存入"]
S3 -->|不为 null| S5["④ 调用 equals 比较"]
S5 -->|相等| S6["不存"]
S5 -->|不相等| S7["存入(挂到链表中)"]
S4 --> S8["⑤ 存满到 16×0.75=12 时 自动扩容为原长度的 2 倍"]| 关键参数 | 默认值 |
|---|---|
| 初始数组长度 | 16 |
| 加载因子 | 0.75 |
| 扩容触发点 | 元素数 = 16 × 0.75 = 12 |
| 扩容规则 | 每次扩为原来的 2 倍 |
注意:JDK 8 之前是新元素占老元素位置、老元素挂下面;JDK 8 之后是新元素直接挂在老元素下面。当链表长度超过 8 且数组长度 ≥ 64时,链表自动转成红黑树提升查询性能。
四、LinkedHashSet 集合
LinkedHashSet依然是基于哈希表(数组 + 链表 + 红黑树)实现的,但每个元素额外多了一个双向链表机制记录前后元素位置——所以保证存取顺序。
- 元素唯一 ✔(继承自
HashSet) - 保证存取顺序 ✔(多了一条双向链表)
- 效率低于
HashSet(多维护一条链表)
五、Collections 工具类
java.util.Collections是集合工具类,提供操作集合的静态方法。
5.1 可变参数(Varargs)
可变参数用在形参中可以接收多个数据,本质在方法内部就是一个数组。
| 格式 | 说明 |
|---|---|
数据类型...参数名称 | 只能写在形参列表中 |
调用方式灵活:可以不传参数、传 1 个或多个、传一个数组。
⚠ 注意事项:
- 一个形参列表中可变参数只能有一个
- 可变参数必须放在形参列表的最后面
public static int sum(int... nums) { // 本质 int[] nums int total = 0; for (int n : nums) total += n; return total;}
sum(); // 0sum(1, 2, 3); // 6sum(new int[]{1,2});// 3六、Map 集合介绍
Map是键值对集合,一次添加两个元素。需要存储一对应的数据时,优先考虑Map。
| 实现类 | 键的特点 |
|---|---|
| HashMap | 按键无序,不重复,无索引 |
| LinkedHashMap | 按键有序,不重复,无索引 |
| TreeMap | 按键排序,不重复,无索引 |
三个实现类的值都不做要求(可重复、可为 null)。
6.1 常见 API
Map是双列集合的顶层接口,它的功能是全部双列集合都可以使用的。
| 方法 | 说明 |
|---|---|
V put(K key, V value) | 添加元素 |
V remove(Object key) | 根据键删除键值对元素 |
void clear() | 移除所有的键值对元素 |
boolean containsKey(Object key) | 判断集合是否包含指定的键 |
boolean containsValue(Object value) | 判断集合是否包含指定的值 |
boolean isEmpty() | 判断集合是否为空 |
int size() | 集合的长度(键值对的个数) |
6.2 三种遍历方式
| 方式 | 核心方法 | 适用场景 |
|---|---|---|
| 通过键找值 | keySet() + get(key) | 只想用键的遍历 |
| 通过键值对对象 | entrySet() + Map.Entry | 同时拿键和值(推荐) |
通过 forEach | forEach(BiConsumer) | Lambda 风格 |
方式 1:通过键找值(分 3 步)
Set<String> keys = map.keySet(); // ① 获取所有键for (String key : keys) { // ② 遍历 Set 集合 String value = map.get(key); // ③ 根据键找值 System.out.println(key + "=" + value);}方式 2:通过键值对对象(核心是 Map.Entry)
// Map.Entry 内部接口的方法K getKey(); // 获取键V getValue(); // 获取值
Set<Map.Entry<String, String>> entries = map.entrySet();for (Map.Entry<String, String> entry : entries) { System.out.println(entry.getKey() + "=" + entry.getValue());}方式 3:通过 forEach(Lambda)
map.forEach((key, value) -> System.out.println(key + "=" + value));七、Map 集合的实现类
| 实现类 | 键的底层 |
|---|---|
| TreeMap | 红黑树 |
| HashMap | 哈希表 |
| LinkedHashMap | 哈希表 + 双向链表 |
7.1 HashMap 底层原理
HashMap与HashSet底层结构完全相同:哈希表。
| 版本 | 结构 | 重要升级 |
|---|---|---|
| JDK 8 之前 | 数组 + 链表 | — |
| JDK 8 开始 | 数组 + 链表 + 红黑树 | 链表长度 > 8 且数组长度 ≥ 64 时自动转红黑树 |
map.put("键", "值") 的存储过程:
flowchart LR
A["利用键计算哈希值 跟值无关"] --> B["根据哈希值与数组长度 计算应存入位置"]
B --> C{"该位置是否 为 null?"}
C -->|是| D["Entry 对象直接存入"]
C -->|否| E["调用 equals 比较"]
E -->|相等| F["值覆盖"]
E -->|不相等| G["挂到链表中"]7.2 LinkedHashMap
底层是哈希表 + 双向链表,与 LinkedHashSet 同理——哈希表保证键唯一,双向链表保证存取顺序。
7.3 TreeMap
底层是红黑树,键按排序组织。Comparable / Comparator 两种排序方式与 TreeSet 一致。
八、Set / List / Map 选择速查
| 需求 | 推荐集合 | 底层 |
|---|---|---|
| 元素可重复 | ArrayList(用的最多) | 数组 |
| 元素可重复,增删明显多于查询 | LinkedList | 链表 |
| 元素去重 | HashSet(用的最多) | 哈希表 |
| 元素去重,且保证存取顺序 | LinkedHashSet | 哈希表 + 双向链表 |
| 元素排序 | TreeSet | 红黑树 |
九、练习:统计字符出现次数
需求:字符串
aababcabcdabcde,统计每个字符出现次数,按a(5)b(4)c(3)d(2)e(1)格式输出。
public class CharCount { public static void main(String[] args) { String s = "aababcabcdabcde"; Map<Character, Integer> map = new LinkedHashMap<>(); // 保证输出顺序 for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); map.merge(c, 1, Integer::sum); // 简化 put 逻辑 }
StringBuilder sb = new StringBuilder(); map.forEach((k, v) -> sb.append(k).append('(').append(v).append(')')); System.out.println(sb); // a(5)b(4)c(3)d(2)e(1) }}注意:用
LinkedHashMap而不是HashMap,是为了保证输出顺序与首次出现顺序一致。
第十三章:Stream 流
Stream 流 配合 Lambda 表达式使用,可以简化集合和数组操作。其思想类似”工厂流水线”:原料(数据)→ 加工(中间方法)→ 出厂(终结方法),每一步都可以灵活组合,写出链式编程风格的代码。
一、Stream 流思想三步骤
flowchart LR A["① 将数据到流中 (获取流对象)"] --> B["② 中间方法 过滤/转换/去重..."] B --> C["③ 终结方法 遍历/统计/收集"]
| 步骤 | 关键点 |
|---|---|
| ① 获取流对象 | 把集合/数组/零散数据”上”到流水线 |
| ② 中间方法 | 调用后返回新的 Stream,可继续链式调用 |
| ③ 终结方法 | 调用后流被消费,链式调用结束 |
二、获取 Stream 流对象
共有 3 种获取方式,根据数据来源选择。
| 数据来源 | 调用方式 | 方法签名 |
|---|---|---|
| 集合 | Collection 接口中的默认方法 | default Stream<E> stream() |
| 数组 | Arrays 工具类的静态方法 | static <T> Stream<T> stream(T[] array) |
| 零散数据 | Stream 接口中的静态方法 | static <T> Stream<T> of(T... values) |
// ① 集合List<String> list = ...;Stream<String> s1 = list.stream();
// ② 数组Integer[] arr = {1, 2, 3};Stream<Integer> s2 = Arrays.stream(arr);
// ③ 零散数据Stream<String> s3 = Stream.of("a", "b", "c");三、Stream 中间操作方法
中间方法调用后返回新的 Stream,支持链式编程。
| 方法签名 | 说明 |
|---|---|
Stream<T> filter(Predicate<? super T> predicate) | 用于对流中的数据进行过滤 |
Stream<T> limit(long maxSize) | 获取前几个元素 |
Stream<T> skip(long n) | 跳过前几个元素 |
Stream<T> distinct() | 去除流中重复的元素(依赖 hashCode 和 equals) |
static <T> Stream<T> concat(Stream a, Stream b) | 合并 a 和 b 两个流为一个流 |
Stream<R> map(Function<? super T, ? extends R> mapper) | 对流中的每一个元素进行转换 |
⚠ 注意事项:如果流对象已经被消费过,就不允许再次使用。每个流对象只能终结一次,多次调用终结方法会抛
IllegalStateException。
// 链式编程:用 limit + concat + distinct 一气呵成Stream<String> s1 = list.stream().limit(4);Stream<String> s2 = list.stream().skip(2);Stream<String> s3 = Stream.concat(s1, s2);s3.distinct().forEach(System.out::println); // 消费 s3 后流就关闭四、Stream 终结操作方法
终结方法调用后流被关闭,链式结束。
| 方法签名 | 说明 |
|---|---|
void forEach(Consumer action) | 对此流的每个元素执行遍历操作 |
long count() | 返回此流中的元素数 |
五、Stream 收集操作
Stream 流操作不会修改数据源,操作完成后若需要重新得到集合/数组,需要用
collect方法。
// 把流中的数据转回集合R collect(Collector collector);Collectors 工具类提供 3 种常用收集方式:
| 静态方法 | 作用 |
|---|---|
public static <T> Collector toList() | 把元素收集到 List 集合中 |
public static <T> Collector toSet() | 把元素收集到 Set 集合中 |
public static Collector toMap(Function keyMapper, Function valueMapper) | 把元素收集到 Map 集合中 |
5.1 收集到 Map 示例
需求:将
ArrayList中的"zhangsan,23"/"lisi,24"/"wangwu,25"收集到Map<姓名, 年龄>。
ArrayList<String> list = new ArrayList<>();Collections.addAll(list, "zhangsan,23", "lisi,24", "wangwu,25");
Map<String, Integer> map = list.stream() .collect(Collectors.toMap( s -> s.split(",")[0], // 键:姓名 s -> Integer.parseInt(s.split(",")[1]) // 值:年龄 ));// {zhangsan=23, lisi=24, wangwu=25}5.2 综合练习:演员信息处理
需求:两个
ArrayList集合分别存储 6 名男演员和 6 名女演员。
- 男演员只要名字为 3 个字的前两人
- 女演员只要姓林的,并且不要第一个
- 把过滤后的男演员姓名和女演员姓名合并到一起
- 把合并后的元素作为构造方法的参数创建
Actor对象,遍历数据- 演员类
Actor含一个成员变量、一个带参构造方法、对应 get / set 方法
// 演员类public class Actor { private String name; public Actor(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; }}
// 处理ArrayList<String> males = new ArrayList<>(Arrays.asList( "蔡徐坤", "李易峰", "王俊凯", "易烊千玺", "陈伟霆", "邓伦"));ArrayList<String> females = new ArrayList<>(Arrays.asList( "林允儿", "林志玲", "杨幂", "赵丽颖", "刘亦菲", "林心如"));
Stream<String> m = males.stream().filter(s -> s.length() == 3).limit(2);Stream<String> f = females.stream().filter(s -> s.startsWith("林")).skip(1);Stream.concat(m, f).map(Actor::new).forEach(a -> System.out.println(a.getName()));第十四章:异常
异常 指的是程序在编译或执行过程中,出现的非正常的情况(错误)。常见如
ArrayIndexOutOfBoundsException/ClassCastException/NullPointerException。语法错误不是异常。
一、异常体系
flowchart TD T["Throwable"] --> E["Error"] T --> X["Exception"] X --> R["RuntimeException 及其子类"] X --> C["除 RuntimeException 之外 所有的异常"]
| 父类 | 子类 | 性质 | 举例 |
|---|---|---|---|
| Error | 严重级别问题 | 无法处理 | StackOverflowError(栈内存溢出)、OutOfMemoryError(堆内存溢出) |
| Exception | 异常类,程序常见错误 | 可处理 | 见下分类 |
1.1 Exception 两大类
| 分类 | 触发时机 | 常见异常 | 原因 |
|---|---|---|---|
运行时异常(RuntimeException 及其子类) | 编译不报错,运行时可能出错 | 数组索引越界 ArrayIndexOutOfBoundsException、空指针 NullPointerException、类型转换 ClassCastException | 程序员代码不严谨 |
编译时异常(除 RuntimeException 外) | 编译阶段就报错 | new FileReader("D:\\A.txt") | 起到提醒作用,需提前处理 |
注意:编译时异常不报错,运行时可能报错;编译时异常没继承 RuntimeException,编译阶段就会出错。
二、异常的默认处理流程
不做任何处理时,JVM 的默认行为:
flowchart TD A["① 虚拟机在出现异常的代码处 自动创建异常对象"] --> B["② 异常从方法出现的点抛出给调用者 调用者最终抛出给 JVM"] B --> C["③ JVM 收到异常后 在控制台输出异常信息"] C --> D["④ 终止 Java 程序运行"]
三、异常处理方式
Java 提供 2 种 主动处理方式:捕获(try…catch)和抛出(throws / throw)。
3.1 try...catch:捕获异常
优点:程序可以继续往下执行。
try { // 可能会出现异常的代码} catch (异常类型1 变量) { // 处理异常的方案} catch (异常类型2 变量) { // 处理异常的方案}⚠ 关键点:如果使用多个 catch,最大的异常需要放在最后(父类在后)。
Throwable 常见方法:
| 方法 | 说明 |
|---|---|
public String getMessage() | 获取异常的错误原因(简短) |
public void printStackTrace() | 展示完整的异常错误信息(最常用) |
3.2 throws:抛出异常
用在方法名后面,声明”这个方法存在异常,但不做处理,交给调用方处理”。
public void method() throws 异常1, 异常2, 异常3... { }⚠ 关键点:子类重写父类方法时,不能抛出父类没有的异常,或者比父类更大的异常。
3.3 两种方式怎么选
| 方式 | 出现问题时 | 选择依据 |
|---|---|---|
| try…catch | 程序可以继续执行 | 问题不需要暴露给调用者 |
| throws 抛出 | 程序在错误点停止 | 问题需要暴露出去 |
思路:看这个问题是否需要暴露出来 → 需要:抛出,不需要:try…catch。
3.4 throw vs throws
| 关键字 | 位置 | 后面跟 | 作用 |
|---|---|---|---|
throw | 方法体中 | 异常对象 | 抛出异常对象 |
throws | 方法名后面 | 异常类型 | 声明方法存在异常 |
细节:抛出的异常对象如果是编译时异常,必须使用
throws声明;如果是运行时异常,则不需要写throws。
四、自定义异常
Java 无法为这个世界上全部的问题提供异常类。当企业想用异常管理某个业务问题时,需要自定义异常类。
4.1 两类自定义异常
| 类型 | 父类 | 步骤 |
|---|---|---|
| 自定义编译时异常 | 继承 Exception | ① 定义类 ② 重写构造器 |
| 自定义运行时异常 | 继承 RuntimeException | ① 定义类 ② 重写构造器 |
4.2 示例:自定义业务异常
// 1. 自定义运行时异常public class AgeOutOfRangeException extends RuntimeException { public AgeOutOfRangeException() { super(); } public AgeOutOfRangeException(String message) { super(message); }}
// 2. 业务方法中抛出public void setAge(int age) { if (age < 0 || age > 150) { throw new AgeOutOfRangeException("年龄超出范围: " + age); } this.age = age;}
// 3. 调用方捕获try { setAge(200);} catch (AgeOutOfRangeException e) { System.out.println(e.getMessage()); // 年龄超出范围: 200}好处:通过自定义异常可以精准表达特定错误(比通用
Exception更有语义)。
第十五章:File 类、递归
File类是操作系统文件对象(文件、文件夹)的 Java 封装,本章先掌握File的 API,再学习递归——用File遍历多层目录的关键。
一、File 类创建对象
File封装的对象仅仅是一个路径名,这个路径可以存在,也可以不存在。
| 构造方法 | 说明 |
|---|---|
public File(String pathname) | 根据文件路径创建文件对象 |
public File(String parent, String child) | 根据父路径 + 子路径字符串创建文件对象 |
public File(File parent, String child) | 根据父路径 File 对象 + 子路径字符串创建文件对象 |
1.1 绝对路径 vs 相对路径
| 路径类型 | 含义 | 示例 |
|---|---|---|
| 绝对路径 | 从盘符根目录开始,到具体的文件或文件夹 | new File("D:\\A.txt") |
| 相对路径 | 相对于当前项目的所在目录 | new File("A.txt")(实际路径是”项目根\A.txt”) |
二、File 类常用 API
2.1 判断方法
| 方法 | 说明 |
|---|---|
public boolean isDirectory() | 判断是否为文件夹 |
public boolean isFile() | 判断是否为文件 |
public boolean exists() | 判断路径是否存在 |
2.2 获取信息方法
| 方法 | 说明 |
|---|---|
public long length() | 返回文件的大小(字节数) |
public String getAbsolutePath() | 返回文件的绝对路径 |
public String getPath() | 返回定义文件时使用的路径 |
public String getName() | 返回文件的名称,带后缀 |
public long lastModified() | 返回文件的最后修改时间(时间毫秒值) |
2.3 创建和删除方法
| 方法 | 说明 |
|---|---|
public boolean createNewFile() | 创建一个新的空文件 |
public boolean mkdir() | 只能创建一级文件夹 |
public boolean mkdirs() | 可以创建多级文件夹(推荐) |
public boolean delete() | 删除文件或空文件夹 |
⚠ 关键点:
delete()方法只能删除空文件夹,且不走回收站(不可恢复)。
2.4 遍历方法
| 方法 | 说明 |
|---|---|
public File[] listFiles() | 获取当前目录下所有的一级文件对象(不包含子目录内容) |
注意:
listFiles()返回值的 4 种情况:
- 路径不存在 → 返回
null- 路径是文件 → 返回
null- 路径是空文件夹 → 返回长度为
0的数组- 路径是需权限才能访问的文件夹 → 返回
null
三、递归
递归 是指方法直接或者间接调用本身。它的核心思路是:将大问题层层转化为与原问题相似、规模更小的问题来解决。
⚠ 关键点:递归如果没有控制好终止,会出现递归死循环,导致栈内存溢出(
StackOverflowError)。
3.1 案例:求 5 的阶乘
推导:5! = 5 × 4!、4! = 4 × 3!、…、1! = 1。
public class RecursionDemo1 { public static void main(String[] args) { int result = jc(5); System.out.println(result); // 120 }
public static int jc(int num) { if (num == 1) { return 1; // 终止条件 } return num * jc(num - 1); // 自己调自己,参数规模更小 }}递归栈帧:
flowchart LR A["jc(5) return 5*24"] --> B["jc(4) return 4*6"] B --> C["jc(3) return 3*2"] C --> D["jc(2) return 2*1"] D --> E["jc(1) return 1"]
3.2 案例:不死神兔(斐波那契数列)
题目:一对兔子,从出生后第 3 个月起每个月都生一对兔子,小兔子长到第 3 个月后又生一对。假设兔子都不死,问第 20 个月的兔子对数为多少?
规律:从第 3 个月开始,兔子对数 = 前两个月之和。
| 月 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | … |
|---|---|---|---|---|---|---|---|---|---|
| 对数 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | … |
public static int rabbit(int month) { if (month == 1 || month == 2) return 1; return rabbit(month - 1) + rabbit(month - 2);}3.3 递归使用场景
常用于逻辑类似的业务:文件遍历、目录树、阶乘、斐波那契等。
四、递归案例实战
4.1 找出所有 .java 文件(含子目录)
需求:键盘录入一个文件夹路径,找出这个文件夹下所有的
.java文件(包括子文件夹)。 思路:单层用listFiles()即可,子文件夹需要递归进入查找。
public static void findJava(File dir, ArrayList<String> list) { File[] files = dir.listFiles(); if (files == null) return;
for (File f : files) { if (f.isFile() && f.getName().endsWith(".java")) { list.add(f.getAbsolutePath()); } else if (f.isDirectory()) { findJava(f, list); // 递归 } }}4.2 递归删除文件夹
注意:
delete()只能删除空文件夹,所以要先删子再删自己(后序遍历)。
public static void deleteDir(File dir) { File[] files = dir.listFiles(); if (files == null) return;
for (File f : files) { if (f.isFile()) { f.delete(); // 文件直接删 } else if (f.isDirectory()) { deleteDir(f); // 递归 } } dir.delete(); // 最后删自己}4.3 递归统计文件夹大小
思路:文件夹 = 所有文件大小之和;遇到子文件夹继续递归。
public static long sizeOf(File dir) { long total = 0; File[] files = dir.listFiles(); if (files == null) return 0;
for (File f : files) { if (f.isFile()) { total += f.length(); } else if (f.isDirectory()) { total += sizeOf(f); } } return total;}五、键盘录入路径案例
需求:键盘录入一个文件夹路径,如果输入错误就给出提示,并继续录入,直到正确为止。
分析:输入的路径可能不存在或是文件路径,所以需用 exists() + isDirectory() 双重判断。
Scanner sc = new Scanner(System.in);File dir;while (true) { System.out.print("请输入文件夹路径: "); String path = sc.nextLine(); dir = new File(path); if (!dir.exists()) { System.out.println("路径不存在,请重新输入"); } else if (!dir.isDirectory()) { System.out.println("不是文件夹,请重新输入"); } else { break; }}System.out.println("已找到文件夹: " + dir.getAbsolutePath());第十六章:常用 API
Java 提供了一系列”工具类”来完成数学计算、系统操作、日期处理等通用任务。本章覆盖
Math、System、包装类、BigDecimal、Arrays、Date、SimpleDateFormat、LocalDateTime/LocalDate/LocalTime、DateTimeFormatter以及时间间隔工具ChronoUnit/Duration/Period。
一、Math 类
Math类是用于执行基本数字运算的工具类,所有方法都是static修饰,可直接通过类名调用。
| 方法名 | 说明 |
|---|---|
public static int abs(int a) | 获取参数绝对值 |
public static double ceil(double a) | 向上取整 |
public static double floor(double a) | 向下取整 |
public static int round(float a) | 四舍五入 |
public static int max(int a, int b) | 获取两个 int 值中的较大值 |
public static double pow(double a, double b) | 返回 a 的 b 次幂的值 |
public static double random() | 返回值为 double 的随机值,范围 [0.0, 1.0) |
二、System 类
System的功能是静态的,都是直接用类名调用即可。
| 方法名 | 说明 |
|---|---|
public static void exit(int status) | 终止当前运行的 Java 虚拟机,非零表示异常终止 |
public static long currentTimeMillis() | 返回当前系统的时间毫秒值形式 |
public static void arraycopy(数据源数组, 起始索引, 目的地数组, 起始索引, 拷贝个数) | 数组拷贝 |
时间原点小知识:计算机中的时间原点是 1970 年 1 月 1 日 08:00:00。1969 年 8 月贝尔实验室的肯·汤普逊利用妻子离开一个月的机会开发了 Unix,使用 B 编译语言在 PDP-7 上开发,随后丹尼斯·里奇改进了 B 语言,开发出 C 语言重写了 Unix,因此 1970 年 1 月 1 日 也算 C 语言的生日。
三、包装类
Java 是面向对象的语言,但基本类型(如
int、double)不是对象,无法调用方法或参与泛型。包装类的作用就是将基本数据类型包装成类(变成引用数据类型),从而可以创建对象、调用方法解决问题。
| 基本数据类型 | 引用数据类型 | 基本数据类型 | 引用数据类型 |
|---|---|---|---|
| byte | Byte | float | Float |
| short | Short | double | Double |
| int | Integer | boolean | Boolean |
| long | Long | char | Character |
⚠ 注意事项:除了
int → Integer和char → Character是”特殊拼写”外,其他都是基本类型首字母大写。
3.1 Integer 类
用于将基本数据类型手动包装为类。
| 构造 / 方法 | 说明 |
|---|---|
public Integer(int value) | |
public static Integer valueOf(int i) | 推荐通过静态方法装箱 |
说明:从 JDK9 开始
new Integer(int)构造方法已被标记为过时,建议使用valueOf或自动装箱。
3.2 自动装箱与自动拆箱
| 名称 | 解释 |
|---|---|
| 自动装箱 | 基本类型的数据和变量可以直接赋值给包装类型的变量 |
| 自动拆箱 | 包装类型的变量可以直接赋值给基本数据类型的变量 |
Integer i = 10; // 自动装箱int n = i; // 自动拆箱int result = i + 100; // 拆箱后参与运算简单记:有了自动拆装箱,基本数据类型和对应的包装类,可以直接运算,操作起来非常便捷。
3.3 包装类练习
需求:已知字符串
String s = "10,50,30,20,40",请将该字符串转换为整数并存入数组,随后求出最大值打印在控制台。
String s = "10,50,30,20,40";String[] arr = s.split(",");int[] nums = new int[arr.length];int max = nums[0];for (int i = 0; i < arr.length; i++) { nums[i] = Integer.parseInt(arr[i]); // 字符串 → int if (nums[i] > max) { max = nums[i]; }}System.out.println("最大值: " + max);四、BigDecimal 类
用于解决小数运算中出现的不精确问题。例如:
double num1 = 0.1;double num2 = 0.2;System.out.println(num1 + num2); // 0.300000000000000044.1 创建对象
| 构造 / 方法 | 说明 |
|---|---|
public BigDecimal(double val) | 通过 double 创建(不推荐,仍有精度问题) |
public BigDecimal(String val) | 通过字符串创建,推荐 |
public static BigDecimal valueOf(double val) | 静态方法,推荐 |
4.2 常用方法
| 方法名 | 说明 |
|---|---|
public BigDecimal add(BigDecimal b) | 加法 |
public BigDecimal subtract(BigDecimal b) | 减法 |
public BigDecimal multiply(BigDecimal b) | 乘法 |
public BigDecimal divide(BigDecimal b) | 除法 |
public BigDecimal divide(另一个 BigDecimal 对象, 精确几位, 舍入模式) | 除法(精确控制) |
4.3 divide 除法细节
除法运算需要注意除不尽的情况,必须指定保留位数与舍入模式。
BigDecimal divide = bd1.divide(参与运算的对象, 小数点后精确到多少位, 舍入模式);| 舍入模式 | 含义 |
|---|---|
RoundingMode.UP | 进一法 |
RoundingMode.DOWN | 去尾法 |
RoundingMode.HALF_UP | 四舍五入 |
BigDecimal a = new BigDecimal("10");BigDecimal b = new BigDecimal("3");BigDecimal r = a.divide(b, 2, RoundingMode.HALF_UP); // 3.33小结:BigDecimal 用于解决小数运算的精度损失问题;创建对象推荐
BigDecimal(String)或static valueOf(double);除法运算要注意除不尽的情况。
五、Arrays 类
数组操作工具类,专门用于操作数组元素。
| 方法名 | 说明 |
|---|---|
public static String toString(类型[] a) | 将数组元素拼接为带格式的字符串 |
public static boolean equals(类型[] a, 类型[] b) | 比较两个数组内容是否相同 |
public static int binarySearch(int[] a, int key) | 查找元素在数组中的索引(二分查找法) |
public static void sort(类型[] a) | 对数组进行默认升序排序 |
5.1 二分查找原理
二分查找的前提:必须是排好序的数据。每次取中间位置元素与目标值比较:
- 中间元素 < 目标值 → 往右半区查找
- 中间元素 > 目标值 → 往左半区查找
- 中间元素 = 目标值 → 找到
flowchart LR A["原始数组 11 22 33 44 55 66 77 88 99 100"] -->|"查找 88 中间=55 比 55 大,往右"| B["右半区 66 77 88 99 100"] B -->|"中间=88 正好相等"| C["找到索引 7"]
⚠ 注意:如果查找的元素不在数组中,
binarySearch会返回负数(即 - (插入点 + 1))。
int[] arr = {11, 22, 33, 44, 55, 66, 77, 88, 99, 100};int idx = Arrays.binarySearch(arr, 88); // 7六、Date 类
Date代表日期和时间,位于java.util包下。
| 构造器 | 说明 |
|---|---|
public Date() | 创建一个 Date 对象,代表系统当前此刻的日期时间 |
public Date(long time) | 把时间毫秒值转换成 Date 日期对象 |
| 常见方法 | 说明 |
|---|---|
public long getTime() | 返回从 1970 年 1 月 1 日 00:00:00 走到此列的总毫秒数 |
public void setTime(long time) | 设置日期对象的时间为当前时间毫秒值对应的时间 |
Date d1 = new Date();System.out.println(d1); // 当前时间System.out.println(d1.getTime()); // 当前时间毫秒值Date d2 = new Date(0L); // 时间原点System.out.println(d2); // Thu Jan 01 08:00:00 CST 1970小结:①
Date对象创建 →new Date();② 获取时间毫秒值 →getTime();③ 时间毫秒值转Date对象 →new Date(long time)或setTime(long time)。
七、SimpleDateFormat 类
用于日期格式化(
Date→ 字符串)和解析(字符串 →Date)。
7.1 构造器与格式化方法
| 分类 | 方法名 | 说明 |
|---|---|---|
| 构造器 | public SimpleDateFormat() | 构造一个 SimpleDateFormat,使用默认格式 |
| 构造器 | public SimpleDateFormat(String pattern) | 构造一个 SimpleDateFormat,使用指定的格式 |
| 格式化方法 | public final String format(Date date) | 将日期格式化成日期/时间字符串 |
| 格式化方法 | public final Date parse(String source) | 将字符串解析为日期类型 |
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");Date d = new Date();String s = sdf.format(d); // Date → 字符串Date d2 = sdf.parse("2025-01-20 15:30:00"); // 字符串 → Date常用格式符号:
yyyy(年)MM(月)dd(日)HH(时)mm(分)ss(秒)。
八、JDK8 新增的时间 API
JDK8 之后新增的时间 API 设计更合理、功能丰富、使用方便,主要特点:
- 都是不可变对象,修改后会返回新的时间对象,不会丢失最开始的时间
- 线程安全
- 能精确到毫秒、纳秒
8.1 LocalDateTime / LocalDate / LocalTime
| 类 | 含义 |
|---|---|
| LocalDate | 代表本地日期(年、月、日、星期) |
| LocalTime | 代表本地时间(时、分、秒、纳秒) |
| LocalDateTime | 代表本地日期 + 时间(年、月、日、星期、时、分、秒、纳秒) |
获取对象:
| 方法 | 示例 |
|---|---|
public static Xxxx now():获取系统当前时间对应的该对象 | LocalDate ld = LocalDate.now();LocalTime lt = LocalTime.now();LocalDateTime ldt = LocalDateTime.now(); |
public static Xxxx of(...):获取指定时间的对象 | LocalDate.of(2099, 11, 11)LocalTime.of(9, 8, 59)LocalDateTime.of(2025, 11, 16, 14, 30, 1) |
获取年月日时分秒相关的方法:
| 方法名 | 功能 |
|---|---|
int getYear() | 获取年份字段 |
Month getMonth() | 使用 Month 枚举获取年份字段 |
int getMonthValue() | 获取 1 到 12 之间的月份字段 |
int getDayOfMonth() | 获取日期字段 |
DayOfWeek getDayOfWeek() | 获取星期几字段,即枚举 DayOfWeek |
int getHour() | 获取当日时间字段 |
int getMinute() | 获取分钟字段 |
int getSecond() | 获取秒字段 |
修改年月日时分秒相关的方法:
LocalDateTime/LocalDate/LocalTime都是不可变的,下列方法返回的是一个新的对象。
| 方法名 | 说明 |
|---|---|
withHour、withMinute、withSecond、withNano | 修改时间,返回新时间对象 |
plusHours、plusMinutes、plusSeconds、plusNanos | 把某个信息加多少,返回新时间对象 |
minusHours、minusMinutes、minusSeconds、minusNanos | 把某个信息减多少,返回新时间对象 |
equals / isBefore / isAfter | 判断 2 个时间对象是否相等,在前还是在后 |
LocalDateTime ldt = LocalDateTime.now();LocalDateTime newLdt = ldt .withHour(10) // 修改小时 .plusDays(7) // 加 7 天 .minusHours(2); // 减 2 小时8.2 DateTimeFormatter
用于时间的格式化和解析,是 JDK8 新增时间 API 的”格式器”。
| 方法名 | 说明 |
|---|---|
static DateTimeFormatter ofPattern(格式) | 获取格式对象 |
String format(时间对象) | 按照指定方式格式化 |
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");LocalDateTime ldt = LocalDateTime.now();String s = dtf.format(ldt); // 格式化LocalDateTime ldt2 = LocalDateTime.parse(s, dtf); // 解析九、时间间隔工具类
用于计算时间间隔,JDK8 提供了三个工具:
| 类 | 用途 |
|---|---|
| Duration | 用于计算两个”时间”间隔(秒、纳秒) |
| Period | 用于计算两个”日期”间隔(年、月、日) |
| ChronoUnit | 用于计算两个”日期”间隔 |
// 计算两个 LocalDateTime 之间相差的天、时、分LocalDateTime start = LocalDateTime.of(2025, 1, 1, 0, 0, 0);LocalDateTime end = LocalDateTime.now();
long days = ChronoUnit.DAYS.between(start, end);long hours = ChronoUnit.HOURS.between(start, end);long mins = ChronoUnit.MINUTES.between(start, end);案例:根据用户填写的生日计算年龄——使用
Period.between(birth, today)取得”年、月、日”三段间隔,进而精确计算周岁。
LocalDate birth = LocalDate.of(1990, 12, 1);LocalDate today = LocalDate.now();Period p = Period.between(birth, today);System.out.println("年龄:" + p.getYears() + " 岁 " + p.getMonths() + " 月 " + p.getDays() + " 天");第十七章:IO 流
IO 是 “Input / Output” 的缩写,意为输入 / 输出,本质是数据传输。常见场景:读写配置文件 / 日志文件、客户端与服务端通讯、文件上传下载等。
一、IO 介绍和分类
1.1 IO 流向
把外存中的数据读取到内存中称为”输入”(读),把内存中的数据写出到外存中称为”输出”(写)。
flowchart LR
subgraph 输入流管道
direction LR
FILE["D:\user.txt"] -->|"读取数据"| JVM["Java 程序"]
end
subgraph 输出流管道
direction RL
FILE2["D:\user.txt"] <==|"写出数据"| JVM2["Java 程序"]
end1.2 IO 流体系结构
IO 流按”数据单位”分为两大体系:字节流(万能流,所有文件都能操作)和字符流(纯文本文件专用,能解决中文乱码)。
| 分类 | 抽象类 | 说明 | 抽象类的子类 |
|---|---|---|---|
| 字节流(万能流) | InputStream | 字节输入流 | FileInputStream |
| 字节流 | OutputStream | 字节输出流 | FileOutputStream |
| 字符流(纯文本) | Reader | 字符输入流 | FileReader |
| 字符流 | Writer | 字符输出流 | FileWriter |
小结:字符流使用场景:读写纯文本文件;字节流使用场景:不是纯文本文件都用字节流。
二、FileOutputStream 字节输出流
FileOutputStream是字节输出流的子类,用于把 Java 程序的数据写出到文件(如D:\A.txt)。
2.1 构造方法
| 构造方法 | 说明 |
|---|---|
FileOutputStream(String name) | 输出流关联文件,文件路径以字符串形式给出 |
FileOutputStream(String name, boolean append) | 第二个参数是追加写入的开关 |
FileOutputStream(File file) | 输出流关联文件,文件路径以 File 对象形式给出 |
FileOutputStream(File file, boolean append) | 第二个参数是追加写入的开关 |
⚠ 注意事项:① 关联的文件如果不存在会自动创建;② 如果文件已存在,默认会清空原有的内容再重新写入(除非开启追加模式)。
2.2 成员方法
| 方法 | 说明 |
|---|---|
void write(int b) | 写出单个字节 |
void write(byte[] b) | 写出一个字节数组 |
void write(byte[] b, int off, int len) | 写出字节数组的一部分 |
// 示例:写出数据FileOutputStream fos = new FileOutputStream("D:\\A.txt", true); // 追加写入fos.write(97); // 写出单个字节 'a'fos.write("Hello".getBytes()); // 写出整个字符串fos.close(); // 关闭流,释放资源三、关流与异常处理
流对象使用完毕后,记得调用
close方法关闭,不然会占用资源。
3.1 JDK7 之后的 try-with-resources 写法
try后面的小括号里可以直接写需要关闭的对象,前提是该对象实现了AutoCloseable接口。
try (FileOutputStream fos = new FileOutputStream("D:\\A.txt")) { fos.write("Hello".getBytes());} catch (IOException e) { e.printStackTrace();}优势:① 自动调用
close()释放资源;② 代码更简洁,避免忘记关流。
四、FileInputStream 字节输入流
FileInputStream是字节输入流,用于读取文件中的数据到 Java 程序(如把D:\A.txt的内容读进来)。
4.1 构造方法
| 构造方法 | 说明 |
|---|---|
FileInputStream(String name) | 输入流关联文件,文件路径以字符串形式给出 |
FileInputStream(File file) | 输入流关联文件,文件路径以 File 对象形式给出 |
⚠ 注意事项:① 关联的文件不存在会抛出
FileNotFoundException异常;② 文件夹的话会拒绝访问。
4.2 成员方法
| 方法 | 说明 |
|---|---|
int read() | 读取一个字节并返回,如果到达文件结尾则返回 -1 |
int read(byte[] b) | 将读取到字节放到传入的数组,返回读取到的有效字节个数,到达文件结尾则返回 -1 |
// 方式一:单字节读取(效率低)try (FileInputStream fis = new FileInputStream("D:\\A.txt")) { int b; while ((b = fis.read()) != -1) { System.out.print((char) b); }} catch (IOException e) { e.printStackTrace();}
// 方式二:字节数组读取(推荐,效率高)try (FileInputStream fis = new FileInputStream("D:\\A.txt")) { byte[] bys = new byte[2]; int len; while ((len = fis.read(bys)) != -1) { System.out.print(new String(bys, 0, len)); }} catch (IOException e) { e.printStackTrace();}4.3 String 构造方法(配合使用)
| 构造方法 | 说明 |
|---|---|
public String(byte[] bytes, int offset, int length) | 将字节数组转换为字符串,参数 1:字节数组;参数 2:起始索引;参数 3:转换的个数 |
五、文件拷贝案例
需求:将
D:\嘿嘿.jpg拷贝到E:\根目录下。
flowchart LR A["1. 创建输入流对象 读取文件"] --> B["2. 创建输出流对象 关联数据目的"] B --> C["3. 读写操作"] C --> D["4. 关流释放资源"]
try ( FileInputStream fis = new FileInputStream("D:\\嘿嘿.jpg"); FileOutputStream fos = new FileOutputStream("E:\\嘿嘿.jpg")) { byte[] bys = new byte[1024]; int len; while ((len = fis.read(bys)) != -1) { fos.write(bys, 0, len); }} catch (IOException e) { e.printStackTrace();}六、FileReader 字符输入流
用于读取纯文本文件,能解决中文乱码问题。
| 分类 | 方法 | 说明 |
|---|---|---|
| 构造 | FileReader(String fileName) | 字符输入流关联文件,路径以字符串形式给出 |
| 构造 | FileReader(File file) | 字符输入流关联文件,路径以 File 对象形式给出 |
| 成员方法 | public int read() | 读取单个字符 |
| 成员方法 | public int read(char[] cbuf) | 读取一个字符数组,返回读取到的有效字符个数 |
七、FileWriter 字符输出流
7.1 构造方法
| 构造方法 | 说明 |
|---|---|
FileWriter(String fileName) | 字符输出流关联文件,路径以字符串形式给出 |
FileWriter(String fileName, boolean append) | 参数 2:追加写入的开关 |
FileWriter(File file) | 字符输出流关联文件,路径以 File 对象形式给出 |
FileWriter(File file, boolean append) | 参数 2:追加写入的开关 |
7.2 成员方法
| 方法 | 说明 |
|---|---|
public void write(int c) | 写出单个字符 |
public void write(char[] cbuf) | 写出一个字符数组 |
public void write(char[] cbuf, int off, int len) | 写出字符数组的一部分 |
public void write(String str) | 写出字符串 |
public void write(String str, int off, int len) | 写出字符串的一部分 |
⚠ 注意事项:字符输出流写出数据,需要调用
flush或close方法,数据才会写出。
flush()后可以继续写出close()后不能继续写出(流已关闭)
八、Properties 集合
Properties是java.util包下的类,继承自Hashtable<Object, Object>,本质是一个 Map 集合,但键和值都必须是 String。
8.1 作为集合使用
| 分类 | 方法 | 说明 |
|---|---|---|
| 构造 | Properties() | 创建一个没有默认值的空属性列表 |
| 成员方法 | Object setProperty(String key, String value) | 添加 / 修改一个键值对 |
| 成员方法 | String getProperty(String key) | 根据键获取值 |
| 成员方法 | Set<String> stringPropertyNames() | 获取集合中所有的键 |
Properties prop = new Properties();prop.setProperty("username", "admin");prop.setProperty("password", "123456");System.out.println(prop.getProperty("username")); // admin8.2 和 IO 有关的方法
| 方法 | 说明 |
|---|---|
void load(InputStream inStream) | 从输入字节流读取属性列表(键和元素对) |
void load(Reader reader) | 从输入字符流读取属性列表(键和元素对) |
void store(OutputStream out, String comments) | 将此属性列表写入输出字节流,以适合 load(InputStream) 方法的格式 |
void store(Writer writer, String comments) | 将此属性列表写入输出字符流,以适合 load(Reader) 方法的格式 |
// 把集合中的键值对写到配置文件Properties prop = new Properties();prop.setProperty("username", "admin");prop.setProperty("password", "123456");try (FileWriter fw = new FileWriter("config.properties")) { prop.store(fw, "user config");}
// 从配置文件中读取键值对到集合Properties prop2 = new Properties();try (FileReader fr = new FileReader("config.properties")) { prop2.load(fr); System.out.println(prop2.getProperty("username"));}小结:①
Properties本质就是 Map 集合,键值都是 String;②setProperty(键,值)添加、getProperty(键)取值;③load从流加载、store写入流——将来加载配置文件时很方便。
九、HuTool 工具
9.1 Hutool 介绍
Hutool 是一个小而全的 Java 工具类库,通过静态方法封装,降低相关 API 的学习成本,提高工作效率。
- 名称由来:
Hutool = Hu + tool,是原公司项目底层代码剥离后的开源库,“Hu” 是公司名称的表示,tool 表示工具- 谐音”糊涂”,一方面简洁易用,一方面寓意”难得糊涂”
9.2 Hutool 常用 API
Hutool 不是 JDK 自带的,使用时需要导入
jar包(后续学习 Maven 可简化此操作)。
IoUtil 类提供的部分方法:
| 方法 | 说明 |
|---|---|
copy(InputStream in, OutputStream out, int bufferSize) | 字节流拷贝 |
copy(Reader reader, Writer writer) | 字符流拷贝 |
readLines(Reader reader, Collection<String> collection) | 按行读取内容到集合 |
close(Closeable closeables) | 安全关闭流 |
FileUtil 类提供的部分方法:
| 方法 | 说明 |
|---|---|
touch(filePath) | 创建文件(自动创建父目录) |
mkdir(dirPath) | 创建目录(支持多级目录) |
copy(srcPath, destPath, isOverride) | 复制文件或目录(可选覆盖) |
move(srcFile, destDir, isOverride) | 移动文件或目录 |
Tips:Hutool 导入 jar 包的方式较为繁琐,后面学到 Maven 的时候可以简化。
第十八章:多线程
多线程是 Java 编程中非常重要的特性,用于提高程序执行效率和同时处理多个任务。本章从进程和线程的概念讲起,逐步深入到 Java 线程的创建、线程安全、线程同步以及线程池的使用。
一、进程和线程
1.1 进程介绍
进程(Process) 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位。简单理解为程序的执行过程。
进程的特点:
| 特点 | 说明 |
|---|---|
| 独立性 | 每一个进程都有自己的空间,在没有经过进程本身允许的情况下,一个进程不可以直接访问其它的进程空间 |
| 动态性 | 进程是动态产生、动态消亡的 |
| 并发性 | 任何进程都可以同其它进程一起并发执行 |
小结:进程是程序的执行过程,具有独立性、动态性、并发性。对于一个 CPU 而言,它是在多个进程间轮换执行的。
1.2 并行和并发
并行:在同一时刻,有多个指令在多个 CPU 上同时执行。 并发:在同一时刻,有多个指令在单个 CPU 上交替执行。
flowchart LR
subgraph 并行
direction LR
A["CPU 1"] --> A1["任务 1"]
B["CPU 2"] --> B1["任务 2"]
end
subgraph 并发
direction LR
C["单个 CPU"] --> C1["任务 1"]
C1 -.-> C2["任务 2"]
C2 -.-> C1
end1.3 线程介绍
线程(Thread):进程可以同时执行多个任务,每个任务就是一个线程。例如:360 安全卫士中的”电脑清理”和”电脑杀毒”是同一个进程中的不同线程。
flowchart TB P["进程"] --> T1["子任务 1(单线程)"] P --> T2["子任务 2(多线程)"] T1 --> S1["子任务 1"] S1 --> S2["子任务 2"] T2 --> S3["子任务 1"] T2 --> S4["子任务 2"]
1.4 多线程的意义
随着处理器上的核心数量越来越多,现在大多数计算机都比以往更加擅长并行计算。一个线程,在一个时刻,只能运行在一个处理器核心上。
多线程的两个核心作用:
| 作用 | 说明 |
|---|---|
| 提高执行效率 | 多个任务并行执行,节省时间 |
| 同时处理多个任务 | 一个应用可以同时处理多种业务(如下载 + 上传) |
小结:线程是进程中的任务,多线程就是多个任务。多线程的意义在于提高效率、可以同时处理多个任务。
二、Java 开启线程的方式
Java 中有三种方式可以开启线程:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口。推荐后两种方式,因为它们扩展性更好(避免了单继承的局限性)。
| 方式 | 是否需要返回值 | 说明 |
|---|---|---|
| 继承 Thread 类 | 无返回值 | 重写 run() 方法,调用 start() 启动线程 |
| 实现 Runnable 接口 | 线程任务无返回值 | 实现 run() 方法,传入 Thread 构造器启动 |
| 实现 Callable 接口 | 线程任务有返回值 | 实现 call() 方法,借助 FutureTask 启动 |
三、线程相关方法
3.1 常用方法
Thread类提供了丰富的线程控制方法,红色标记的是静态方法。
| 方法名称 | 说明 |
|---|---|
String getName() | 返回此线程的名称 |
void setName(String name) | 设置线程的名字(构造方法也可以设置名字) |
static Thread currentThread() | 获取当前线程的对象 |
static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒 |
setPriority(int newPriority) | 设置线程的优先级 |
final int getPriority() | 获取线程的优先级 |
final void setDaemon(boolean on) | 设置为守护线程 |
// 示例:获取并设置线程名MyThread t1 = new MyThread("线程A");MyThread t2 = new MyThread("线程B");
t1.setName("高铁");t2.setName("飞机");
System.out.println(t1.getName()); // 高铁System.out.println(t2.getName()); // 飞机
// Thread.currentThread():获取当前线程对象System.out.println(Thread.currentThread().getName()); // main3.2 线程调度方式
Java 线程调度采用抢占式调度(线程优先级高的先获取 CPU 资源),但结果随机(因为多线程有随机性)。
| 调度方式 | 说明 |
|---|---|
| 抢占式调度 | 优先级高的线程先抢占 CPU,Java 采用此方式 |
| 非抢占式调度 | 按顺序执行,公平但效率低 |
优先级范围:1 ~ 10,默认值为 5。优先级高只是获取 CPU 资源的概率高,不是绝对先执行。
MyThread t1 = new MyThread("高铁");MyThread t2 = new MyThread("飞机");
t1.setPriority(Thread.MIN_PRIORITY); // 1t2.setPriority(Thread.MAX_PRIORITY); // 10
System.out.println(t1.getPriority()); // 1System.out.println(t2.getPriority()); // 10四、线程安全和同步
4.1 线程安全的问题
需求:某电影院目前正在上映国产大片,共有 100 张票,而它有 3 个窗口卖票。请设计一个程序模拟该电影院卖票。
class Tickets implements Runnable { int tickets = 1;
@Override public void run() { while (true) { if (tickets <= 0) { break; } System.out.println(Thread.currentThread().getName() + "卖出了第" + tickets + "号票"); tickets--; } }}上面的代码存在线程安全问题:多个线程同时操作共享数据
tickets,可能导致卖出同一张票甚至卖出 0 号、-1 号票。
4.2 线程安全问题分析
三个线程同时进入
while循环,都读取到tickets = 1,于是都”卖出了第 1 号票”,然后tickets--多次,结果出现重复卖票或卖出非法票号。
安全问题出现的条件:
| 条件 | 说明 |
|---|---|
| 是多线程环境 | 必须有多个线程并发执行 |
| 有共享数据 | 多个线程操作同一个数据 |
| 有多条语句操作共享数据 | 多个操作共享数据的代码 |
4.3 线程同步
同步技术:将多条语句操作共享数据的代码锁起来,让任意时刻只能有一个线程可以执行。
flowchart TB
subgraph 同步前
direction TB
T1["线程 1"] -->|"进入"| Code["多条语句操作共享数据"]
T2["线程 2"] -->|"进入"| Code
T3["线程 3"] -->|"进入"| Code
end
subgraph 同步后
direction TB
T1A["线程 1"] -->|"加锁"| LockA["🔒 锁"]
LockA -->|"执行"| CodeA["多条语句操作共享数据"]
T2A["线程 2"] -.等待.-> LockA
T3A["线程 3"] -.等待.-> LockA
endJava 提供三种同步方式:
| 方式 | 说明 |
|---|---|
| 同步代码块 | synchronized(锁对象) { ... } |
| 同步方法 | 在方法返回值前加 synchronized 关键字 |
| Lock 锁 | java.util.concurrent.locks.Lock 接口,更灵活 |
4.3.1 同步代码块
synchronized(锁对象) { 多条语句操作共享数据的代码}⚠ 注意事项:
- 锁对象可以是任意对象,但是需要保证多条线程的锁对象,是同一把锁
- 同步可以解决多线程的数据安全问题,但是也会降低程序的运行效率
class Tickets implements Runnable { int tickets = 100; Object lock = new Object(); // 锁对象
@Override public void run() { while (true) { synchronized (lock) { if (tickets <= 0) { break; } System.out.println(Thread.currentThread().getName() + "卖出了第" + tickets + "号票"); tickets--; } } }}4.3.2 同步方法
在方法的返回值类型前面加入
synchronized关键字。
public synchronized void method() { // 多条语句操作共享数据的代码}小细节:
- 方法分为静态和非静态
- 静态方法的锁对象是字节码对象(
类名.class)- 非静态方法的锁对象是
this
4.3.3 Lock 锁
使用
Lock锁,我们可以更清晰地看到哪里加了锁,哪里释放了锁。相比synchronized,Lock 锁更灵活、性能更好。
对比:
// 方式一:同步代码块synchronized(锁对象) { 多条语句操作共享数据的代码}
// 方式二:同步方法public synchronized void method() { }
// 方式三:Lock 锁lock.lock();...lock.unlock();
Lock是接口,无法直接创建对象。
| 构造方法 | 说明 |
|---|---|
public ReentrantLock() | 创建一个 ReentrantLock 的实例 |
| 成员方法 | 说明 |
|---|---|
void lock() | 加锁 |
void unlock(); | 释放锁 |
import java.util.concurrent.locks.ReentrantLock;import java.util.concurrent.locks.Lock;
class Tickets implements Runnable { int tickets = 100; Lock lock = new ReentrantLock();
@Override public void run() { while (true) { lock.lock(); try { if (tickets <= 0) { break; } System.out.println(Thread.currentThread().getName() + "卖出了第" + tickets + "号票"); tickets--; } finally { lock.unlock(); // 无论是否异常,都需要释放锁 } } }}五、线程池介绍
5.1 线程池的好处
池:提前创建好的一组资源(如数据库连接池、常量池)。将线程对象交给线程池维护,可以降低系统成本,从而提升程序的性能。
为什么需要线程池:
| 问题 | 说明 |
|---|---|
| 创建成本高 | 系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互 |
| 资源浪费 | 当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程,就会严重浪费系统资源 |
5.2 线程池学习路径
学习线程池分三步:理解线程池的好处 → 使用 JDK 提供的线程池 → 自定义线程池。
flowchart LR A["理解线程池的好处"] --> B["使用 JDK 提供的线程池"] B --> C["自定义线程池"]
⚠【强制】线程池不允许使用
Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明:
Executors返回的线程池对象的弊端如下:
FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。CachedThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
5.3 使用 JDK 提供的线程池
Executors中提供静态方法来创建线程池(了解即可,实际开发推荐使用ThreadPoolExecutor)。
| 方法 | 介绍 |
|---|---|
static ExecutorService newCachedThreadPool() | 创建一个默认的线程池 |
static newFixedThreadPool(int nThreads) | 创建一个指定最多线程数量的线程池 |
六、自定义线程池
6.1 ThreadPoolExecutor 类
ThreadPoolExecutor是线程池的核心实现类,其构造方法有 7 个参数。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)6.2 参数详解
| 参数 | 含义 | 约束 |
|---|---|---|
| 参数 1 | 核心线程数量(正式员工) | 不能小于 0 |
| 参数 2 | 最大线程数量(正式员工 + 临时工) | 不能小于等于 0,最大数量 >= 核心线程数量 |
| 参数 3 | 空闲时间 | 不能小于 0 |
| 参数 4 | 时间单位 | 时间单位 |
| 参数 5 | 任务队列(指定排队人数) | 不能为 null |
| 参数 6 | 线程对象工厂 | 不能为 null |
| 参数 7 | 拒绝策略 | 不能为 null |
flowchart TB Customers["顾客队列"] -->|"新任务"| Core["核心线程(正式员工) 数 = corePoolSize"] Core -->|"核心线程满"| Queue["任务队列 BlockingQueue"] Queue -->|"队列满"| Temp["临时线程 (maximumPoolSize - corePoolSize)"] Temp -->|"全部忙 + 队列满"| Reject["拒绝策略 RejectedExecutionHandler"] Temp -->|"空闲 keepAliveTime"| Temp Core -->|"空闲 keepAliveTime"| Core
6.3 线程池拒绝策略
| 策略选项 | 说明 |
|---|---|
ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出 RejectedExecutionException 异常(默认) |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常,这是不推荐的做法 |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务,然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy | 调用任务的 run() 方法,绕过线程池直接执行 |
6.4 自定义线程池小结
思考:临时线程什么时候创建?什么时候会开启拒绝策略?
| 问题 | 触发条件 |
|---|---|
| 临时线程什么时候创建? | 线程任务数 > 核心线程数 + 任务队列的数量 |
| 什么时候会开启拒绝策略? | 线程任务数 > 最大线程数 + 任务队列的数量 |
flowchart TB
T["提交新任务"] --> Q1{"核心线程 是否已满?"}
Q1 -->|"否"| C1["创建核心线程执行"]
Q1 -->|"是"| Q2{"任务队列 是否已满?"}
Q2 -->|"否"| Q3["任务进入队列等待"]
Q2 -->|"是"| Q4{"当前线程数 是否达到最大?"}
Q4 -->|"否"| C2["创建临时线程执行"]
Q4 -->|"是"| Q5["执行拒绝策略"]小结:自定义线程池通过
ThreadPoolExecutor的 7 个参数灵活控制线程池的行为。核心线程处理正常业务,任务队列起到缓冲作用,临时线程应对突发流量,拒绝策略保证系统在极端情况下不会崩溃。
第十九章:网络编程
网络编程是实现设备间数据通信的核心技术,本章从通信架构、网络三要素讲起,重点掌握 UDP 和 TCP 两种协议的数据收发方式。
一、网络编程基本概念
1.1 网络编程
网络编程是让设备中的程序与网络上其他设备中的程序进行数据交互的技术(实现网络通信)。典型场景:手机与手机聊天、网页访问服务器。
1.2 基本的通信架构
基本的通信架构有 2 种形式:CS 架构、BS 架构。
| 架构 | 全称 | 典型应用 | 特点 |
|---|---|---|---|
| CS 架构 | Client 客户端 / Server 服务端 | 微信、IntelliJ IDEA、网易云音乐 | 需要程序员开发客户端软件;需要用户下载安装客户端软件 |
| BS 架构 | Browser 浏览器 / Server 服务端 | Chrome、Edge、火狐浏览器访问网页 | 不需要程序员开发客户端;需要用户下载安装浏览器 |
⚠ 无论是 CS 架构,还是 BS 架构的软件都必须依赖网络编程!Java 提供了哪些网络编程解决方案?
java.net.*包下提供了网络编程的解决方案!
flowchart LR
subgraph CS架构
direction LR
A1["Client 客户端"] -->|"请求"| B1["Server 服务端"]
B1 -->|"响应"| A1
end
subgraph BS架构
direction LR
A2["Browser 浏览器"] -->|"HTTP请求"| B2["Server 服务端"]
B2 -->|"HTTP响应"| A2
end二、网络编程三要素
网络通信三要素:IP 地址、端口、协议。
| 要素 | 含义 |
|---|---|
| 01 IP 地址 | 设备在网络中的地址,是设备在网络中的唯一标识 |
| 02 端口 | 应用程序在设备中的唯一标识 |
| 03 协议 | 连接和数据在网络中传输的规则 |
2.1 IP 地址
IP(Internet Protocol):全称”互联网协议地址”,是分配给上网设备的唯一标识。目前,被广泛采用的 IP 地址形式有两种:IPv4、IPv6。
| 类别 | 位数 | 字节数 | 表示法 | 数量 |
|---|---|---|---|---|
| IPv4 | 32 位 | 4 个字节 | 点分十进制(如 192.168.1.66) | 最多 2^32 个,目前已用完 |
| IPv6 | 128 位 | 16 个字节 | 冒分十六进制(如 2001:0db8:0000:0023:0008:0800:200c:417a) | 最多 2^128 个,可为地球上每一粒沙子编号 |
小细节:IPv4 地址一般写成
11000000.10101000.00000001.01000010,转成十进制就是192.168.1.66。
2.1.1 IPv4 地址的分类形式
| 分类 | 说明 | 范围 |
|---|---|---|
| 公网地址 | 万维网使用 | — |
| 私有地址 | 局域网使用,专门为组织机构内部使用,以此节省 IP | 192.168.0.0 ~ 192.168.255.255 |
小结:常见场景为网吧、公寓、商场、办公楼等小型组织内部使用 192.168.xxx.xxx 的私有地址。
2.1.2 特殊 IP 地址
127.0.0.1(也可以是localhost):是回送地址也称本地回环地址,也称本机 IP,永远只会寻找当前所在本机。 建议:自己练习就写127.0.0.1。
flowchart LR A["你的电脑"] -->|"访问其他电脑"| B["192.168.1.10"] A -->|"访问自己"| C["127.0.0.1"]
2.1.3 IP 和域名
平时上网没有用过 IP,都是用网址的。浏览器访问服务器时,先通过 DNS 服务器把网址(域名)解析成 IP 地址,再去访问对应的服务器。
flowchart LR Browser["浏览器"] -->|"网址/域名"| DNS["DNS 服务器"] DNS -->|"IP 地址"| Browser Browser -->|"访问黑马程序员服务器"| Server["黑马程序员服务器"] Server -->|"返回数据"| Browser
2.1.4 常用的 CMD 命令
| 命令 | 说明 |
|---|---|
ipconfig | 查看本机 IP 地址 |
ping | 检查网络是否连通 |
小结:
- 现在如何解决 IPv4 不够的问题? 利用局域网 IP 解决 IP 不够的问题
- 特殊的 IP 是什么?
127.0.0.1(永远表示本机)- 常见的两个 CMD 命令?
ipconfig、ping
2.1.5 InetAddress 类
为了方便对 IP 地址的获取和操作,Java 提供了
InetAddress类。InetAddress:此类表示 Internet 协议(IP)地址。
| 方法名 | 说明 |
|---|---|
static InetAddress getByName(String host) | 确定主机名称的 IP 地址。主机名称可以是机器名称,也可以是 IP 地址 |
String getHostName() | 获取此 IP 地址的主机名 |
String getHostAddress() | 返回文本显示中的 IP 地址字符串 |
import java.net.InetAddress;
public class InetAddressDemo { public static void main(String[] args) throws Exception { InetAddress address = InetAddress.getByName("DESKTOP-ABC123"); // InetAddress address = InetAddress.getByName("192.168.1.100");
System.out.println(address.getHostName()); // 主机名 System.out.println(address.getHostAddress()); // IP 地址字符串 }}2.2 端口号
端口号是应用程序在设备中唯一的标识。
| 项目 | 说明 |
|---|---|
| 范围 | 0 ~ 65535(由两个字节表示的整数) |
| 知名端口 | 0 ~ 1023 用于一些知名的网络服务或者应用 |
| 自定义端口 | 1024 以上 端口号自己使用 |
⚠ 注意:一个端口号只能被一个应用程序使用。
flowchart LR
subgraph 电脑A
APP1["WeChat 6666"] --> P65533["65533 消息"]
APP2["QQ 7777"] --> P65533
APP3["Game 8888"] --> P65533
end
P65533 -->|"端口号"| 电脑B2.3 协议
计算机网络中,连接和通信的规则被称为网络通信协议。常见的协议有 UDP 协议 和 TCP 协议。
2.3.1 UDP 协议
用户数据报协议(User Datagram Protocol)。
- 面向无连接的通信协议
- 速度快,有大小限制(一次最多发送 64K)
- 数据不安全,易丢失数据
- 不管是否已经连接成功,都会发送
- 典型应用:在线视频
2.3.2 TCP 协议
传输控制协议 TCP(Transmission Control Protocol)。
- 面向连接的通信协议
- 速度慢,没有大小限制,数据安全
- 通过三次握手建立连接
- 通过四次挥手断开连接
- 典型应用:下载软件
flowchart LR
subgraph 三次握手
direction TB
A1["客户端"] -->|"① 连接请求"| B1["服务器"]
B1 -->|"② 响应"| A1
A1 -->|"③ 确认"| B1
end
subgraph 四次挥手
direction TB
A2["客户端"] -->|"① 取消请求"| B2["服务器"]
B2 -->|"② 响应"| A2
B2 -->|"③ 处理数据后确认"| A2
A2 -->|"④ 确认"| B2
end小结:网络通信三要素是 IP 地址、端口、协议,三者缺一不可。
三、UDP 协议收发数据
3.1 UDP 协议发送数据
类比快递流程:① 找快递公司 → ② 打包礼物 → ③ 快递公司发送包裹 → ④ 付钱走人。
| 步骤 | 类比 | 代码对应 |
|---|---|---|
| ① 找快递公司 | 找快递公司 | 创建发送端的 DatagramSocket 对象 |
| ② 打包礼物 | 打包礼物 | 数据打包(DatagramPacket) |
| ③ 快递公司发送包裹 | 快递公司发送包裹 | 发送数据 |
| ④ 付钱走人 | 付钱走人 | 释放资源 |
import java.net.DatagramPacket;import java.net.DatagramSocket;import java.net.InetAddress;
public class UDPSend { public static void main(String[] args) throws Exception { // ① 创建发送端的 DatagramSocket 对象 DatagramSocket ds = new DatagramSocket();
// ② 打包礼物(数据) String msg = "你好 UDP"; byte[] bytes = msg.getBytes(); InetAddress address = InetAddress.getByName("127.0.0.1"); int port = 10086;
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
// ③ 发送数据 ds.send(dp);
// ④ 释放资源 ds.close(); }}3.2 UDP 协议接收数据
类比快递流程:① 找快递公司 → ② 接收箱子 → ③ 从箱子里面获取礼物 → ④ 签收走人。
| 步骤 | 类比 | 代码对应 |
|---|---|---|
| ① 找快递公司 | 找快递公司 | 创建接收端的 DatagramSocket 对象 |
| ② 接收箱子 | 接收箱子 | 接收打包好的数据 |
| ③ 从箱子里面获取礼物 | 从箱子里面获取礼物 | 解析数据包 |
| ④ 签收走人 | 签收走人 | 释放资源 |
import java.net.DatagramPacket;import java.net.DatagramSocket;
public class UDPReceive { public static void main(String[] args) throws Exception { // ① 创建接收端的 DatagramSocket 对象 // 必须指定端口号,跟发送端一致 DatagramSocket ds = new DatagramSocket(10086);
// ② 接收打包好的数据 byte[] bytes = new byte[1024]; DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
// 该方法是阻塞的,会一直等发送端发送数据 ds.receive(dp);
// ③ 解析数据包 byte[] data = dp.getData(); int len = dp.getLength(); String msg = new String(data, 0, len); System.out.println("数据是:" + msg);
// ④ 释放资源 ds.close(); }}小结:UDP 协议收发数据
- 使用
DatagramSocket搭建站点- 使用
DatagramPacket打包数据send方法用于发送receive方法用于接收
四、TCP 协议收发数据
4.1 TCP 通信程序
TCP 通信严格区分客户端和服务端。
4.1.1 客户端
| 步骤 | 说明 | API |
|---|---|---|
| 1 | 创建 Socket 对象指定 IP 和端口 | Socket(String host, int port) |
| 2 | 通过 socket 对象获取传输数据的流对象 | OutputStream getOutputStream()、InputStream getInputStream() |
| 3 | 通过流对象收发数据 | — |
| 4 | 释放资源 | — |
提示:这里的流对象不是读写文件中的数据
- 输出流:写出数据到服务端
- 输入流:读取服务端发送过来的数据
4.1.2 服务端
| 步骤 | 说明 | API |
|---|---|---|
| 1 | 创建 ServerSocket 对象指定端口 | ServerSocket(int port) |
| 2 | 响应客户端发送的请求 | Socket accept() |
| 3 | 通过 socket 对象获取传输数据的流对象 | OutputStream getOutputStream()、InputStream getInputStream() |
| 4 | 通过流对象收发数据 | — |
| 5 | 释放资源 | — |
// ================ 客户端 ================import java.io.OutputStream;import java.net.Socket;
public class TCPClient { public static void main(String[] args) throws Exception { // 1. 创建 Socket 对象指定 IP 和端口 Socket socket = new Socket("127.0.0.1", 10086);
// 2. 通过 socket 对象获取输出流 OutputStream os = socket.getOutputStream();
// 3. 写出数据 os.write("你好 TCP".getBytes());
// 4. 释放资源 os.close(); socket.close(); }}
// ================ 服务端 ================import java.io.InputStream;import java.net.ServerSocket;import java.net.Socket;
public class TCPServer { public static void main(String[] args) throws Exception { // 1. 创建 ServerSocket 对象指定端口 ServerSocket ss = new ServerSocket(10086);
// 2. 响应客户端发送的请求,得到 Socket 对象 Socket socket = ss.accept();
// 3. 通过 socket 对象获取输入流 InputStream is = socket.getInputStream();
// 4. 读取数据 byte[] bytes = new byte[1024]; int len = is.read(bytes); String msg = new String(bytes, 0, len); System.out.println("客户端发来:" + msg);
// 5. 释放资源 is.close(); socket.close(); ss.close(); }}小结:TCP 协议区分客户端和服务端
- 客户端创建
Socket对象,指定 IP 和端口号- 服务端创建
ServerSocket对象,指定端口号- 调用
accept()响应请求,得到Socket对象- 两端通过
Socket对象获取输入输出流,收发数据
第二十章:Java 高级技术
本章是 Java 高级特性的综合章节,涵盖枚举、类加载器、反射、注解、动态代理等核心技术。这些技术是框架的基石,掌握它们可以更深入地理解框架的运行原理。
一、枚举
1.1 枚举介绍
枚举是 Java 中的一种特殊类型,一般用来做信息的标记和分类。例如用标记来区分春、夏、秋、冬四个季节。
传统方式 vs 枚举方式:
// 传统方式:使用 public static final 常量class Season { public static final int SPRING = 1; public static final int SUMMER = 2; public static final int AUTUMN = 3; public static final int WINTER = 4;}
// 枚举方式:使用 enum 关键字enum Season { SPRING, SUMMER, AUTUMN, WINTER;}1.2 枚举定义格式
枚举本质上是一个类,但比类多了几个特殊限制。
修饰符 enum 枚举类名 { 枚举项1, 枚举项2, 枚举项3...;}public enum Season { SPRING, SUMMER, AUTUMN, WINTER;}1.3 枚举的特点
| 特点 | 说明 |
|---|---|
| 每一个枚举项其实就是该枚举的一个对象 | 默认 public static final 修饰 |
| 通过枚举类名去访问指定的枚举项 | Season.SPRING |
所有枚举类都是 Enum 的子类 | 继承自 java.lang.Enum |
| 枚举也是类,可以定义成员变量 | — |
| 枚举类第一行必须是枚举项 | 最后一个枚举项后的分号可以省略,但有其他内容时不能省略 |
枚举类可以有构造器,但必须是 private 的 | 默认也是 private |
| 枚举类可以有抽象方法 | 但枚举项必须重写该方法 |
1.4 枚举的使用场景
枚举最适合用于做信息的标记和分类。
场景:商品状态有 在售/预售/售罄 三种情况。
// 传统方式:定义多个常量public static final int PRE_SALE = 1; // 预售public static final int SALE = 2; // 在售public static final int SELLOUT = 3; // 售罄
// 枚举方式:一目了然enum GoodState { PRE_SALE, // 预售 SALE, // 在售 SELLOUT // 售罄}小结:使用枚举做信息的标记和分类,代码可读性更高,入参约束严谨,代码优雅。
二、类加载器
2.1 类加载器介绍
类加载器负责将类的字节码载入方法区中。加载时机为:用到就加载。
flowchart LR A[Java 源文件] -->|javac 编译| B[class 字节码文件] B -->|类加载器载入| C[方法区]
加载时机(满足以下任一条件即可):
| 条件 | 示例 |
|---|---|
| 创建对象 | Student stu = new Student(); |
| 调用类的静态成员 | System.out.println(A.num); |
| 初始化继承体系 | Zi z = new Zi();(Zi extends Fu) |
| 使用反射加载类的字节码 | Class.forName("com.xxx"); |
2.2 类加载器的加载过程
类的加载过程分三步:加载 → 链接(验证、准备、解析) → 初始化。
flowchart LR A[加载] -->|找到 class| B[链接] B --> C[验证] C --> D[准备] D --> E[解析] E --> F[初始化]
| 阶段 | 功能 |
|---|---|
| 加载 | 通过包名 + 类名获取这个类,准备用流进行传输;将类加载到内存中;加载完毕创建一个 Class 对象 |
| 链接 - 验证 | 验证类是否符合 JVM 规范,安全性检查 |
| 链接 - 准备 | 为 static 变量分配空间,设置默认值(如 String school = null) |
| 链接 - 解析 | 将常量池中的符号引用解析为直接引用 |
| 初始化 | 根据程序员程序编码制定的主观计划去初始化类变量和其他资源(如 String school = "传智大学") |
小工具:
jps是 JDK 提供的一个查看当前 Java 进程的小工具。HSDB(Hotspot Debugger)是一款内置于 SA 中的 GUI 调试工具,可用于调试 JVM 运行时数据。启动命令:Terminal window java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
2.3 类加载器的分类
| 类加载器 | 说明 | 负责范围 |
|---|---|---|
| Bootstrap class loader | 启动类加载器,虚拟机的内置类加载器,通常表示为 null | C++ 实现,获取到的只能是 null |
| Platform class loader | 平台类加载器,负责加载 JDK 中一些特殊的模块(JDK9 之前是 Extension Class Loader) | 负责加载 lib\modules 内部的类(JDK9 之前是 jre\lib\ext 目录下的类) |
| Application class loader | 应用程序类加载器 | 负责加载自己写的类 |
| 自定义类加载器 | 上级为 Application | 目前不做讲解 |
获取类加载器:
| 方法 | 功能 |
|---|---|
public ClassLoader getClassLoader() | 获取该类的类加载器对象 |
2.4 双亲委派模式
双亲委派模式是 Java 类加载器的核心机制。
flowchart TB
Start["开始 AppClassLoader"] --> A1{"是否加载过?"}
A1 -->|是| Loaded["已经加载过 不再加载"]
A1 -->|否| A2{"是否可以加载?"}
A2 -->|否| P1["向上委托 PlatformClassLoader"]
A2 -->|是| Done["自己加载此类"]
P1 --> P2{"是否加载过?"}
P2 -->|是| Loaded
P2 -->|否| P3{"是否可以加载?"}
P3 -->|否| B1["向上委托 BootStrapClassLoader"]
P3 -->|是| Done
B1 --> B2{"是否加载过?"}
B2 -->|是| Loaded
B2 -->|否| B3{"是否可以加载?"}
B3 -->|否| CNF["ClassNotFoundException"]
B3 -->|是| Done双亲委派模式的原则:
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载
- 而是把这个请求委托给父类的加载器去执行
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归
- 请求最终将到达顶层的启动类加载器
- 如果父类加载器可以完成类加载任务,就成功返回
- 倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
- 避免类的重复加载
小结:
- 类加载器负责将类的字节码载入到方法区中
- 加载时机:用到就加载
- 加载过程:加载 → 链接(验证、准备、解析) → 初始化
三、反射
3.1 反射介绍
反射是框架技术的灵魂。反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意属性和方法。这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。
理解:注释是给人看的,注解是给虚拟机看的。反射操作的是字节码对象。
flowchart LR Byte["字节码对象"] -->|"解剖"| A1["构造方法对象"] Byte --> A2["成员变量对象"] Byte --> A3["成员方法对象"] A1 -->|"创建"| A1a["创建对象"] A2 -->|"赋值、获取"| A2a["赋值 获取"] A3 -->|"调用"| A3a["调用"]
3.2 反射技术的学习路径
- 获取类的字节码对象
- 反射类中的构造方法,随后创建对象
- 反射类中的成员变量,完成赋值和获取
- 反射类中的成员方法,调用成员方法
3.3 获取类的字节码对象
获取
Class对象有三种方式。
| 方式 | 说明 | 适用场景 |
|---|---|---|
Class.forName("全类名") | 全类名(包名+类名) | 编译时还不知道类名(最常用) |
类名.class | 通过类名直接获取 | 编译时已确定类名 |
对象.getClass() | 通过对象获取 | 已有对象时使用 |
// 1. Class.forName(最常用)Class<?> clazz1 = Class.forName("com.itheima.Student");
// 2. 类名.classClass<?> clazz2 = Student.class;
// 3. 对象.getClass()Student stu = new Student();Class<?> clazz3 = stu.getClass();小结:反射是框架技术的灵魂,在运行状态中可以操作任意类中的任意属性任意方法;反射操作的是字节码对象;获取字节码对象的三种方式见上表。
3.4 反射类中的构造方法
| 方法 | 功能 |
|---|---|
Constructor<?>[] getConstructors() | 返回所有公共构造方法对象的数组 |
Constructor<?>[] getDeclaredConstructors() | 返回所有构造方法对象的数组 |
Constructor<T> getConstructor(Class<?>... parameterTypes) | 返回单个公共构造方法对象 |
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) | 返回单个构造方法对象 |
Constructor 类用于创建对象的方法:
| 方法 | 功能 |
|---|---|
T newInstance(Object... initargs) | 根据指定的构造方法创建对象 |
setAccessible(boolean flag) | 设置为 true,表示取消访问检查(暴力反射) |
// 通过反射获取无参构造方法并创建对象Class<?> clazz = Class.forName("com.itheima.Student");Constructor<?> con = clazz.getDeclaredConstructor();con.setAccessible(true); // 取消访问检查Object obj = con.newInstance();⚠ 反射建议:禁止暴力反射操作(生产环境慎用
setAccessible(true))。
3.5 反射类中的成员变量
| 方法 | 功能 |
|---|---|
Field[] getFields() | 返回所有公共成员变量对象的数组 |
Field[] getDeclaredFields() | 返回所有成员变量对象的数组 |
Field getField(String name) | 返回单个公共成员变量对象 |
Field getDeclaredField(String name) | 返回单个成员变量对象 |
Field 类的设置和获取方法:
| 方法 | 功能 |
|---|---|
void set(Object obj, Object value) | 赋值 |
Object get(Object obj) | 获取值 |
Class<?> clazz = Class.forName("com.itheima.Student");Object obj = clazz.getDeclaredConstructor().newInstance();
// 反射获取 name 成员变量并赋值Field nameField = clazz.getDeclaredField("name");nameField.setAccessible(true);nameField.set(obj, "张三");String name = (String) nameField.get(obj);System.out.println(name); // 张三3.6 反射类中的成员方法
| 方法 | 功能 |
|---|---|
Method[] getMethods() | 返回所有公共成员方法对象的数组,包括继承的 |
Method[] getDeclaredMethods() | 返回所有成员方法对象的数组,不包括继承的 |
Method getMethod(String name, Class<?>... parameterTypes) | 返回单个公共成员方法对象 |
Method getDeclaredMethod(String name, Class<?>... parameterTypes) | 返回单个成员方法对象 |
Method 类用于执行方法的方法:
| 方法 | 功能 |
|---|---|
Object invoke(Object obj, Object... args) | 运行方法 |
Class<?> clazz = Class.forName("com.itheima.Student");Object obj = clazz.getDeclaredConstructor().newInstance();
// 反射调用 study 方法Method method = clazz.getDeclaredMethod("study");method.setAccessible(true);method.invoke(obj); // 调用 study()3.7 反射练习
练习 1(伪泛型):请向一个泛型为
Integer的集合,添加一个String字符串。 思路:Java 中的泛型是假的,只在编译的时候有效。
ArrayList<Integer> list = new ArrayList<>();list.add(1);list.add(2);
// 通过反射绕过泛型检查Class<?> clazz = list.getClass();Method add = clazz.getMethod("add", Object.class);add.invoke(list, "hello");
System.out.println(list); // [1, 2, hello]练习 2(配置文件 + 反射):编写
Student/Teacher/Worker三个 JavaBean(属性均为name、age),通过修改配置文件实现不修改代码切换调用不同类的不同方法。
className=com.itheima.StudentmethodName=studypublic class ReflectDemo { public static void main(String[] args) throws Exception { // 1. 读取配置文件 Properties prop = new Properties(); prop.load(new FileInputStream("config.properties")); String className = prop.getProperty("className"); String methodName = prop.getProperty("methodName");
// 2. 通过反射创建对象 Class<?> clazz = Class.forName(className); Constructor<?> con = clazz.getDeclaredConstructor(); Object obj = con.newInstance();
// 3. 通过反射调用方法 Method method = clazz.getDeclaredMethod(methodName); method.invoke(obj); }}四、注解
4.1 注解介绍
Annotation 表示注解,是 JDK 1.5 的新特性。注解的主要作用是对程序进行标注。
理解:
- 注释是给人看的,注解是给虚拟机看的
- 通过注解可以给类增加额外的信息
- 编译器或 JVM 可以根据注解来完成对应的功能
JDK 中常见注解:
| 注解 | 说明 |
|---|---|
@Override | 表示方法的重写(编译时检查) |
@Deprecated | 表示修饰的方法已过时 |
@SuppressWarnings("all") | 压制警告 |
4.2 自定义注解
重要:自定义注解单独存在意义不大,一般会跟反射结合起来使用。
4.2.1 自定义注解格式
public @interface 注解名称 { public 属性类型 属性名() default 默认值;}public @interface Anno { String show() default "show...";}属性类型:
| 类型 | 说明 |
|---|---|
| 基本数据类型 | int、boolean 等 |
| String | 字符串 |
| Class | 类对象 |
| 注解 | 嵌套注解 |
| 枚举 | enum 类型 |
| 以上类型的一维数组 | 如 int[]、String[] |
完整示例:
public @interface MyAnno { public static final int num1 = 100; public static final String num2 = "abc"; public static final MyAnno num3 = null; public static final Class num4 = String.class; public static final int[] num5 = {};
public abstract String show1() default "show1"; public abstract int show2() default 132; public abstract MyAnno2 show3() default @MyAnno2; public abstract Class show4() default String.class; public abstract int[] show5() default {1, 2, 3};}4.2.2 自定义注解使用
常用位置:
- 类
- 方法
⚠ 注意:
- 在使用注解时,如果注解的属性没有给出默认值,需要手动给出
@Anno(name="张三")- 如果数组中只有一个属性值,在使用时
{}是可以省略的
4.2.3 特殊的属性 value
如果只有一个属性名字为
value没有赋值,使用时直接给出值,不需要写属性名。
public @interface SuppressWarnings { String[] value();}
// 简化写法@SuppressWarnings("all")public class MyClass { }4.3 元注解
元注解就是用在注解上的注解。
| 元注解名 | 说明 |
|---|---|
@Target | 指定了注解能哪里使用 |
@Retention | 可以理解为保留时间(生命周期) |
4.3.1 @Target
@Target:用来标识注解使用的位置,如果没有使用该注解标识,则自定义的注解可以使用在任意位置。
@Target 可使用的值定义在 ElementType 枚举类中:
| 值 | 说明 |
|---|---|
TYPE | 类,接口 |
FIELD | 成员变量 |
METHOD | 成员方法 |
PARAMETER | 方法参数 |
CONSTRUCTOR | 构造方法 |
LOCAL_VARIABLE | 局部变量 |
4.3.2 @Retention
@Retention:用来标识注解的生命周期(有效范围)。
@Retention 可使用的值定义在 RetentionPolicy 枚举类中:
| 值 | 说明 |
|---|---|
SOURCE | 注解只作用在源码阶段,生成的字节码文件中不存在 |
CLASS | 注解作用在源码阶段,字节码文件阶段,运行阶段不存在,默认值 |
RUNTIME | 注解作用在源码阶段,字节码文件阶段,运行阶段(反射需要使用) |
自定义测试注解案例:
需求:自定义一个注解
@Test,用于指定类的方法上,如果类中的某个方法上使用了该注解,就执行该方法。
// 1. 自定义注解@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Test {}
// 2. 使用注解public class UserTest { @Test public void test1() { System.out.println("test1 执行了"); }
public void test2() { System.out.println("test2 没执行"); }}
// 3. 通过反射执行public static void main(String[] args) throws Exception { Class<?> clazz = UserTest.class; Object obj = clazz.getDeclaredConstructor().newInstance(); for (Method method : clazz.getDeclaredMethods()) { if (method.isAnnotationPresent(Test.class)) { method.invoke(obj); } }}五、动态代理
5.1 动态代理
动态代理是对象的另外一种创建方式,这种方式可以在不修改源码的情况下,增强方法。
创建对象的方式:
Dog d = new Dog(); // 1. newAnimal a = new Dog(); // 2. 多态Dog.class.getConstructor().newInstance(); // 3. 反射5.2 代理
代理:在原有功能基础上进行增强。例如:房产中介可以在房东原有售卖房产、房屋出租的功能基础上,额外提供代驾、喂养宠物等服务。
// 1. 共同的接口public interface Sale { void sale(double money); void hire(double money);}
// 2. 被代理对象(房东)public class Owner implements Sale { @Override public void sale(double money) { System.out.println("售卖房产" + money + "元"); }
@Override public void hire(double money) { System.out.println("出租房产" + money + "元"); }}
// 3. 代理对象(中介)public class SaleImpl implements Sale { private Owner owner;
public SaleImpl(Owner owner) { this.owner = owner; }
@Override public void sale(double money) { money += 20000; // 代理对象对功能增强(中介费) owner.sale(money); // 被代理对象处理执行原有功能 }
@Override public void hire(double money) { money += 1000; // 代理对象对功能增强 owner.hire(money); // 被代理对象处理执行原有功能 }}5.3 JDK 动态代理
动态代理在运行时动态生成代理类,无需手动为每个目标类编写代理类。
Java 提供了
java.lang.reflect.Proxy类,提供了使用动态代理方式创建对象的 API。
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)参数介绍:
| 参数 | 作用 |
|---|---|
| 参数一 | 用于指定用哪个类加载器,去加载生成的实现类 |
| 参数二 | 指定接口,这些接口用于指定生成的实现类中有什么,也就是有哪些方法 |
| 参数三 | 用来指定生成的实现类中不同方法的实现方案 |
完整示例:
public class ProxyUtil { public static <T> T createProxy(T obj) { // 获取被代理对象的 ClassLoader ClassLoader loader = obj.getClass().getClassLoader(); // 获取被代理对象实现的所有接口 Class<?>[] interfaces = obj.getClass().getInterfaces(); // 创建 InvocationHandler InvocationHandler h = (proxy, method, args) -> { // 增强逻辑 if (method.getName().equals("sale")) { System.out.println("收取中介费 20000 元"); } // 调用被代理对象的原方法 return method.invoke(obj, args); }; // 返回代理对象 return (T) Proxy.newProxyInstance(loader, interfaces, h); }}
public static void main(String[] args) { Owner owner = new Owner(); Sale sale = ProxyUtil.createProxy(owner); sale.sale(1000000); // 输出:收取中介费 20000 元 / 售卖房产1020000.0元 sale.hire(3000); // 输出:出租房产3000.0元}小结:
- 动态代理是对象的另外一种创建方式,这种方式可以在不修改源码的情况下,增强方法
- 代理对象可以在原有功能的基础上进行增强
- 动态代理在运行时动态生成代理类,无需手动为每个目标类编写代理类
- 使用
Proxy中的newProxyInstance()方法创建代理对象
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 7 天
部分内容可能已过时
评论