21208 字
106 分钟
Java 核心与 AI 开发进阶

Java核心与AI开发进阶#

第十二章:集合高级#

集合是长度可变的容器。Java 集合按”单列/双列”分为两大体系:单列(一次添加一个元素,对应 Collection 接口)和双列(一次添加两个元素即键值对,对应 Map 接口)。本篇覆盖 CollectionList、常用数据结构、ArrayList / LinkedList 以及泛型基础。

一、集合体系结构#

整体划分

分类顶层接口子接口 / 实现类一次添加特点
单列集合CollectionList:ArrayList、LinkedList1 个元素有序
单列集合CollectionSet:TreeSet、HashSet、LinkedHashSet1 个元素无序
双列集合MapTreeMap、HashMap、LinkedHashMap2 个元素(键值对)键不可重复

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所有 CollectionhasNext() 判断 + 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 方法
// 错误写法:会抛 ConcurrentModificationException
Iterator<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):

  1. 数据 B 对应的下一个数据地址指向数据 C
  2. 数据 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 泛型的好处#

  1. 统一数据类型
  2. 将运行期的错误提升到编译期(编译时就能发现类型不匹配)

⚠ 注意事项泛型中只能编写引用数据类型,不能写基本类型(要用包装类:intIntegerdoubleDouble)。

// ❌ 错误
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 泛型方法#

场景泛型声明方式
非静态方法泛型根据类的泛型去匹配
静态方法需要自己声明独立的泛型(静态先于对象存在,无法用类的泛型)
// 非静态:使用类上声明的 E
public class ArrayList<E> {
public boolean add(E e) { ... }
}
// 静态:必须自己声明 <T>
public static <T> void printArray(T[] array) { ... }

7.4 泛型接口#

类实现带泛型的接口时,有两种操作方式:

  1. 实现接口时直接确定类型public class ArrayList<E> implements List<E>
  2. 保持接口的泛型,等创建对象时再确定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)。

自然排序三步骤

  1. 类实现 Comparable 接口
  2. 重写 compareTo 方法
  3. 根据 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 hashCodeequals 配合流程#

哈希值是一个 int 类型的随机值,Java 中每个对象都有一个哈希值(由 Object.hashCode() 底层 C++ 计算)。自定义对象必须按”内容”实现 hashCode + equals,否则重复元素识别不出来。

hashCode 的作用:定位元素应存入哪个桶(数组下标);equals 的作用:同一桶内的元素去重判断

@Override
public 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);
}
@Override
public 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 个或多个、传一个数组。

⚠ 注意事项

  1. 一个形参列表中可变参数只能有一个
  2. 可变参数必须放在形参列表的最后面
public static int sum(int... nums) { // 本质 int[] nums
int total = 0;
for (int n : nums) total += n;
return total;
}
sum(); // 0
sum(1, 2, 3); // 6
sum(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同时拿键和值(推荐)
通过 forEachforEach(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 底层原理#

HashMapHashSet 底层结构完全相同:哈希表。

版本结构重要升级
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()去除流中重复的元素(依赖 hashCodeequals
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 名女演员。

  1. 男演员只要名字为 3 个字的前两人
  2. 女演员只要姓林的,并且不要第一个
  3. 把过滤后的男演员姓名和女演员姓名合并到一起
  4. 把合并后的元素作为构造方法的参数创建 Actor 对象,遍历数据
  5. 演员类 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 个月开始,兔子对数 = 前两个月之和。

12345678
对数1123581321
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 提供了一系列”工具类”来完成数学计算、系统操作、日期处理等通用任务。本章覆盖 MathSystem、包装类、BigDecimalArraysDateSimpleDateFormatLocalDateTime / LocalDate / LocalTimeDateTimeFormatter 以及时间间隔工具 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 是面向对象的语言,但基本类型(如 intdouble)不是对象,无法调用方法或参与泛型。包装类的作用就是将基本数据类型包装成类(变成引用数据类型),从而可以创建对象、调用方法解决问题。

基本数据类型引用数据类型基本数据类型引用数据类型
byteBytefloatFloat
shortShortdoubleDouble
intIntegerbooleanBoolean
longLongcharCharacter

⚠ 注意事项:除了 int → Integerchar → 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.30000000000000004

4.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 设计更合理、功能丰富、使用方便,主要特点:

  1. 都是不可变对象,修改后会返回新的时间对象,不会丢失最开始的时间
  2. 线程安全
  3. 能精确到毫秒、纳秒

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 都是不可变的,下列方法返回的是一个新的对象

方法名说明
withHourwithMinutewithSecondwithNano修改时间,返回新时间对象
plusHoursplusMinutesplusSecondsplusNanos把某个信息多少,返回新时间对象
minusHoursminusMinutesminusSecondsminusNanos把某个信息多少,返回新时间对象
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 程序"]
  end

1.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)写出字符串的一部分

⚠ 注意事项:字符输出流写出数据,需要调用 flushclose 方法,数据才会写出

  • flush() 后可以继续写出
  • close() 后不能继续写出(流已关闭)

八、Properties 集合#

Propertiesjava.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")); // admin

8.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
  end

1.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()); // main

3.2 线程调度方式#

Java 线程调度采用抢占式调度(线程优先级高的先获取 CPU 资源),但结果随机(因为多线程有随机性)。

调度方式说明
抢占式调度优先级高的线程先抢占 CPU,Java 采用此方式
非抢占式调度按顺序执行,公平但效率低

优先级范围1 ~ 10默认值为 5。优先级高只是获取 CPU 资源的概率高,不是绝对先执行。

MyThread t1 = new MyThread("高铁");
MyThread t2 = new MyThread("飞机");
t1.setPriority(Thread.MIN_PRIORITY); // 1
t2.setPriority(Thread.MAX_PRIORITY); // 10
System.out.println(t1.getPriority()); // 1
System.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
  end

Java 提供三种同步方式:

方式说明
同步代码块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 返回的线程池对象的弊端如下:

  1. FixedThreadPoolSingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE可能会堆积大量的请求,从而导致 OOM
  2. 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 个参数灵活控制线程池的行为。核心线程处理正常业务,任务队列起到缓冲作用,临时线程应对突发流量,拒绝策略保证系统在极端情况下不会崩溃。

第十九章:网络编程#

网络编程是实现设备间数据通信的核心技术,本章从通信架构、网络三要素讲起,重点掌握 UDPTCP 两种协议的数据收发方式。

一、网络编程基本概念#

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 地址形式有两种:IPv4IPv6

类别位数字节数表示法数量
IPv432 位4 个字节点分十进制(如 192.168.1.66最多 2^32 个,目前已用完
IPv6128 位16 个字节冒分十六进制(如 2001:0db8:0000:0023:0008:0800:200c:417a最多 2^128 个,可为地球上每一粒沙子编号

小细节:IPv4 地址一般写成 11000000.10101000.00000001.01000010,转成十进制就是 192.168.1.66

2.1.1 IPv4 地址的分类形式#
分类说明范围
公网地址万维网使用
私有地址局域网使用,专门为组织机构内部使用,以此节省 IP192.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 命令? ipconfigping
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 -->|"端口号"| 电脑B

2.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启动类加载器,虚拟机的内置类加载器,通常表示为 nullC++ 实现,获取到的只能是 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

双亲委派模式的原则

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载
  2. 而是把这个请求委托给父类的加载器去执行
  3. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归
  4. 请求最终将到达顶层的启动类加载器
  5. 如果父类加载器可以完成类加载任务,就成功返回
  6. 倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
  7. 避免类的重复加载

小结

  • 类加载器负责将类的字节码载入到方法区中
  • 加载时机:用到就加载
  • 加载过程加载链接(验证、准备、解析)初始化

三、反射#

3.1 反射介绍#

反射框架技术的灵魂。反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意属性和方法。这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。

理解:注释是给人看的,注解是给虚拟机看的。反射操作的是字节码对象

flowchart LR
  Byte["字节码对象"] -->|"解剖"| A1["构造方法对象"]
  Byte --> A2["成员变量对象"]
  Byte --> A3["成员方法对象"]
  A1 -->|"创建"| A1a["创建对象"]
  A2 -->|"赋值、获取"| A2a["赋值 获取"]
  A3 -->|"调用"| A3a["调用"]

3.2 反射技术的学习路径#

  1. 获取类的字节码对象
  2. 反射类中的构造方法,随后创建对象
  3. 反射类中的成员变量,完成赋值和获取
  4. 反射类中的成员方法,调用成员方法

3.3 获取类的字节码对象#

获取 Class 对象有三种方式

方式说明适用场景
Class.forName("全类名")全类名(包名+类名)编译时还不知道类名(最常用
类名.class通过类名直接获取编译时已确定类名
对象.getClass()通过对象获取已有对象时使用
// 1. Class.forName(最常用)
Class<?> clazz1 = Class.forName("com.itheima.Student");
// 2. 类名.class
Class<?> 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(属性均为 nameage),通过修改配置文件实现不修改代码切换调用不同类的不同方法。

config.properties
className=com.itheima.Student
methodName=study
public 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...";
}

属性类型

类型说明
基本数据类型intboolean
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 自定义注解使用#

常用位置

  1. 方法

⚠ 注意

  • 在使用注解时,如果注解的属性没有给出默认值,需要手动给出
  • @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. new
Animal 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() 方法创建代理对象
Java 核心与 AI 开发进阶
https://xianhe51920.github.io/posts/java-core-ai-advanced/
作者
仙鹤
发布于
2026-09-06
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

评论

正在加载评论...