视频加载失败

Java 进阶 Day03:集合、Stream、IO 流与线程入门

3194 字
16 分钟
Java 进阶 Day03:集合、Stream、IO 流与线程入门

List 集合深入#

List 接口提供带索引的增删改查:

List<String> list = new ArrayList<>(); // 经典代码
list.add("hello"); list.add(1, "javaee"); // 按下标插入(第二个位置)
list.remove(2); // 按下标删除
list.set(2, "C"); // 按下标修改
String s = list.get(2); // 按下标获取

四种遍历:for + get(i) / 增强 for / list.forEach(System.out::println) / 迭代器。

List 家族独有:有索引,所以能按下标插入/删除/修改/取(Set 做不到)。

Set 家族与自定义去重#

Set 三个实现类#

实现类特点场景
HashSet无序、不重复、无索引用的最多,去重
LinkedHashSet插入有序、不重复、无索引要保序的去重
TreeSet默认按大小升序、不重复、无索引要排序的去重

HashSet 底层:JDK8 之前是默认长度 16 的数组、加载因子 0.75、数组名 table;JDK8 之后是数组 + 链表 + 红黑树。LinkedHashSet 每个元素额外用一个双链表记录前后,所以能保插入顺序。

自定义对象去重#

两个内容相同的 Student 对象,默认会被 Set 当成两个元素:

Student s1 = new Student("张三",18,"233");
Student s2 = new Student("张三",18,"233"); // 逻辑相同
Set<Student> set = new HashSet<>();
set.add(s1); set.add(s2); set.add(s3); ...
System.out.println(set); // 张三只出现一次 → 去重成功

要让去重生效,Student 必须成对重写 equals 和 hashCode:

@Override
public boolean equals(Object o) {
if (this == o) return true; // 同一个对象,短路
if (o == null || getClass() != o.getClass()) return false; // 类型不同直接 false
Student student = (Student) o;
return age == student.age
&& Objects.equals(name, student.name)
&& Objects.equals(address, student.address); // 字段逐个比
}
@Override
public int hashCode() {
return Objects.hash(name, age, address); // 参与 equals 的字段都要进 hash
}

去重流程:add 先算 hashCode → 定位桶(数组下标)→ 桶空直接放;桶里有元素 → 逐个 equals 比较 → 有相同的就丢弃(重复),全不同就挂上去。

为什么必须成对重写:

  • 只重写 equals 不写 hashCode → 逻辑相同的对象 hashCode 不同 → 分到不同桶 → 永远碰不上 → 去重失效
  • 只重写 hashCode 不写 equals → 同桶但 equals 判 false → 也不去重
  • 哈希约定:equals 相等的对象,hashCode 必须相等(反向不成立:hashCode 相同而 equals 不等 = 哈希冲突,靠链表/红黑树兜底)

HashSet 底层的几个阈值#

  • 初始数组 16,加载因子 0.75
  • 元素个数 > 12(16×0.75)→ 触发扩容(数组翻倍 resize)
  • 链表长度 > 8 且数组长度 ≥ 64 → 链表转红黑树;链表 > 8 但数组没到 64 → 先扩容不转树
  • 12 是扩容阈值,不是树化阈值——两对数字别混(8 和 64 才是树化)

TreeSet 自定义排序#

两种指定排序规则的方式:对象所属类实现 Comparable 接口重写 compareTo;或者构造器传 Comparator 比较器(优先用)。

Set<Teacher> set = new TreeSet<>(new Comparator<Teacher>() {
@Override
public int compare(Teacher o1, Teacher o2) {
return Double.compare(o2.getSalary(), o1.getSalary()); // 工资倒序
}
});

Double.compare 比手写 if (a > b) return 1; 干净(double 不能直接减)。升序写 Double.compare(o1, o2),倒序交换两个参数。

Map 家族#

Map 三个实现类#

实现类特点
HashMap无序、不重复、无索引(用的最多)
LinkedHashMap插入有序、不重复、无索引
TreeMap按键大小升序、不重复、无索引
Map<String,Integer> map = new LinkedHashMap<>(); // 经典代码
map.put("Python",45);
map.put("Python",35); // 键重复 → 覆盖旧值(值可以是任意)
map.put(null, null); // 键和值都可以为 null

键决定特性:键无序/有序/排序,键不重复;值无所谓(可重复、可 null)。

常用方法#

方法语义
put(k,v)键不存在→添加;键已存在→覆盖并返回旧值
putIfAbsent(k,v)仅键不存在时才添加
replace(k,v)仅键已存在时才替换(与 put 相反)
get(k)键不存在返回 null(无法区分「没这键」和「值就是 null」)
getOrDefault(k, 默认)键不存在返回默认值——安全取值的首选,避免 NPE
containsKey(k)O(1) 快,优先用它判断
containsValue(v)遍历整个 Map,性能差,少用
remove(k) / remove(k,v)按键删 / 键值都匹配才删(返回 boolean)
merge(k,v,函数)词频统计神器:freq.merge(w, 1, Integer::sum)
keySet()返回键的 Set(视图,不能 add,会抛 UnsupportedOperationException)
values()返回值的 Collection(值可重复,所以不是 Set)

put 返回值是被覆盖的旧值:

Integer old = map.put("张三", 95); // 张三已存在 → 返回被覆盖的旧值 90
Integer r = map.put("新人", 1); // 键不存在 → 返回 null

三种遍历方式#

方式代码特点
keySet 键找值for (String k : map.keySet()) map.get(k)每次 get 都再查一次哈希,略慢
entrySet 键值对for (Map.Entry k-v : map.entrySet())一次拿到键值对,零查找,推荐
forEach lambdamap.forEach((k,v) -> ...)最简洁,JDK8+

Stream 流#

固定套路:三大步骤#

创建流 → 中间操作 → 终止操作

List<String> list1 = list.stream() // 创建
.filter(name -> name.startsWith("张")) // 中间:过滤
.filter(s -> s.length() == 3) // 中间:再过滤(可链式)
.collect(Collectors.toList()); // 终止:收集成 List
  • 创建:list.stream() / map.keySet().stream() / map.values().stream() / map.entrySet().stream() / Stream.of(arr) / Arrays.stream(arr)
  • 中间操作:filter 过滤 / sorted() 默认升序 + 自定义 Comparator / limit(n) 取前 n / skip(n) 跳过前 n / distinct() 去重 / map 加工
  • 终止操作:forEach / count 计数 / max/min(返回 Optional,.get() 取)/ collect 收集

收集目标#

List<String> list1 = s1.collect(Collectors.toList());
Set<String> set = s2.collect(Collectors.toSet());
String[] arr = s3.toArray(String[]::new);
Map<String,Double> map = stream.collect(Collectors.toMap(Teacher::getName, Teacher::getSalary));

惰性求值#

中间操作都是惰性的(lazy)——list.stream().filter(...) 这一行不会真正遍历,只是「记下规则」;只有终止操作(collect/forEach/count)触发时才真正跑完整条管道。没有终止操作,filter 等于没写。

一次性#

Stream 不能重复消费:被终止操作消费后再使用,抛 IllegalStateException(要多次收集就得每次重新 list.stream())。

IO 流#

File 类#

File f1 = new File("G/nicshot/5.jpg"); // 代表文件/目录(只是个"代表",不代表真实存在)
f1.getName(); f1.length(); f1.isFile(); f1.isDirectory();
  • 绝对路径:带盘符;相对路径:不带盘符,默认从工程目录找
  • 创建/删除:createNewFile() / delete()

字符集编码与解码#

byte[] bytes = name.getBytes("GBK"); // 编码:字符串 → 字节(不指定字符集时默认 UTF-8)
String s = new String(bytes, "GBK"); // 解码:字节 → 字符串
  • 编码解码用的字符集必须一致,否则乱码
  • 中文宽度:GBK 占 2 字节、UTF-8 占 3 字节(一个汉字)

字节流#

InputStream in = new FileInputStream(new File("...")); // 字节输入
byte[] bytes = new byte[1024];
int len;
while ((len = in.read(bytes)) != -1) { // 批量读(比单字节快)
System.out.print(new String(bytes, 0, len));
}
OutputStream out = new FileOutputStream(path, true); // 第二个参数 true = 追加模式
out.write(97); // 写单字节
out.write("我爱你中国".getBytes()); // 写字节数组
out.write(bytes, 0, 3); // 写字节数组的一部分
  • 单字节 read():性能差,且中文必乱码(一个汉字被拆成多个字节读)
  • write('龙') 直接写字符也会乱码——字节流眼里只有字节

字符流#

try (Reader fr = new FileReader(path)) { // try-with-resources 自动关闭
char[] chs = new char[1024];
int len;
while ((len = fr.read(chs)) != -1) {
System.out.print(new String(chs, 0, len));
}
}
try (FileWriter fw = new FileWriter(path, true)) { // true 追加
fw.write("hello world"); fw.write('蒋');
fw.write(chars, 0, 3); // 字符数组一部分
fw.flush(); // 刷缓冲
}
  • 字符流 = 字节流 + 字符集解码:底层还是字节,包了一层编码转换,所以读文本不乱码
  • 字符流也可能乱码:如果缓冲边界正好切开一个汉字(读到不完整的字符)——批量读时常见
  • 读二进制(图片/音视频)必须用字节流;读文本用字符流

缓冲流#

try (Reader fr = new FileReader(path);
BufferedReader br = new BufferedReader(fr)) { // 包装低级流
String line;
while ((line = br.readLine()) != null) { // 按行读(白送的能力)
System.out.println(line);
}
}
  • 缓冲流是处理流,包装节点流(FileInput/OutputStream、FileReader/Writer)
  • 作用:减少底层 IO 次数(每次真实读写都涉及系统调用,慢),缓冲攒一批再读写
  • BufferedReader.readLine() 按行读;BufferedWriter.newLine() 换行

文件复制#

try (FileInputStream fis = new FileInputStream(srcPath);
FileOutputStream fos = new FileOutputStream(destPath)) {
byte[] bytes = new byte[1024];
int len;
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len); // 读多少字节,就写多少字节
}
}

缓冲版:把 fis/fos 再包一层 BufferedInputStream/OutputStream,性能更好。

Apache Commons IO#

FileUtils.copyFile(new File(src), new File(dest)); // 一行复制

用第三方库前先在 pom.xml 加依赖,否则编译失败(commons-io 引入 2.16.1 这类版本号明确的依赖)。

递归#

阶乘与累加#

public static int func(int n) { // 计算 5!
if (n == 1) return 1; // ① 递归出口(终止条件)
return n * func(n - 1); // ② 递归方程
}
public static int func2(int n) { // 1+2+...+n
if (n == 1) return 1;
return func2(n - 1) + n;
}

两要素:出口 + 递归方程;没有出口 → 无限递归 → StackOverflowError 栈溢出(每层递归压一帧进调用栈,栈满就崩)。

文件递归搜索#

public static void searchFile(File dir, String name) {
if (dir == null || !dir.exists() || !dir.isDirectory()) return; // 极端情况防御
File[] files = dir.listFiles();
if (files != null && files.length > 0) {
for (File file : files) {
if (file.isFile() && file.getName().contains(name)) {
System.out.println(file.getAbsolutePath());
} else if (file.isDirectory()) {
searchFile(file, name); // 目录 → 递归进入
}
}
}
}

分级处理:文件 → 判断匹配;目录 → 递归进入;防御 null / 不存在 / 非目录。

线程入门#

创建方式一:继承 Thread#

class MyThread extends Thread {
@Override
public void run() { // 重写 run,写线程任务
for (int i = 0; i < 6; i++) {
System.out.println("线程1正在执行" + i);
}
}
}
Thread t = new MyThread();
t.start(); // 启动线程(不是调 run!)
for (int i = 0; i < 5; i++) {
System.out.println("主线程在跑" + i); // 主线程继续跑
}

start() 与 run() 的区别#

调用行为
t.start()创建新线程,JVM 分配独立执行栈,start 立即返回,与主线程并发交替执行
t.run() 直接调不创建线程,主线程同步执行方法体 = 普通方法调用

执行顺序不保证,随机(线程调度由操作系统决定)。

重点速记#

  1. List 带索引:add(i,ele) / remove(i) / set(i,ele) / get(i);四种遍历
  2. Set 三兄弟:HashSet 无序 / LinkedHashSet 插入有序 / TreeSet 排序,都不重复无索引
  3. 自定义去重铁律:equals + hashCode 成对重写(参与 equals 的字段全进 hashCode)
  4. HashSet 底层:初始 16 / 加载因子 0.75 / 12 扩容;链表 >8 且数组 ≥64 才红黑树化
  5. Map 三兄弟:HashMap 无序(用的最多)/ LinkedHashMap 有序 / TreeMap 按键排序;键决定特性
  6. Map 三遍历:keySet(每次 get 重查哈希)/ entrySet(推荐,零查找) / forEach lambda
  7. put 返回值 = 被覆盖的旧值;键不存在返回 null
  8. getOrDefault 安全取值首选;merge 词频统计;containsKey O(1) 优先
  9. Stream 三步骤:创建 → 中间 → 终止;链式 + 一次性(不能重复消费)
  10. Stream 惰性求值:中间操作 lazy,终止操作才真正遍历
  11. Stream 常用:filter / sorted / limit / skip / distinct / map + collect(toList/toSet/toMap/toArray)
  12. IO 四类流:字节 In/Out(InputStream/OutputStream)+ 字符 In/Out(Reader/Writer)
  13. 中文宽度:GBK 2 字节 / UTF-8 3 字节;字节流切分汉字必乱码;读二进制用字节流、读文本用字符流
  14. 缓冲流:包装节点流,减少底层 IO 次数;readLine() 按行读
  15. try-with-resources:try(资源) 自动关闭,免手动 close
  16. 递归:出口 + 递归方程;无出口 → 栈溢出
  17. 线程:继承 Thread + 重写 run + start();start 开新线程 vs run 同步调用;执行顺序随机

实战:文章数据的文件持久化#

把 IO 流落到控制台博客项目:文章数据落盘,解决「重启数据不丢」。整体流程:仓库预置数据后立即保存一次;新增/删除先改内存列表再整体覆盖写回文件;启动时若文件存在则加载,并把 nextId 恢复为现有最大 id + 1(三轮重启验证:首跑落盘、二跑加载、删除后再启动新文章 id 不撞车)。

设计要点:

决策理由
分隔符用 ~~~ 而非 |文章内容可能含 | 或换行,~~~ 出现概率极低(简单方案不做转义)
显式指定 UTF-8FileWriter 默认平台编码(Windows 是 GBK)→ 中文乱码;字符流 = 字节流 + 字符集
一行一篇文章简单、可读、易排错
枚举存 name()、还原 valueOf()字符串 ↔ 枚举双向转换,编译期安全
@AllArgsConstructor 还原 final 字段createTime 是 final,Lombok 全参构造可还原 id/createTime/status
全量覆盖写简单方案,数据量小无性能问题
nextId 恢复 = max+1防新文章 id 撞车(删除后 id 可复用)

踩坑记录#

  • 工具类的 save 方法误写成 private:工具方法是给外部类调用的,必须 public
  • 文件位置硬编码绝对路径不可移植,应改用相对路径
  • 预置数据后忘记立即落盘,导致文件在写入第一篇新文章前一直是空的
  • 删除逻辑把「保存」写在 return 之后(save(articles); return remove(i);),save 永远不会执行,删除无法落盘;正确顺序是 remove → save → return
  • @AllArgsConstructor 与手写的部分参构造器可以共存,用 Lombok 全参构造还原 final 字段即可

已知限制:文章内容含换行时按行 split 会截断;数据量大时可改为增量写或引入数据库。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
Java 进阶 Day03:集合、Stream、IO 流与线程入门
https://ryuki.icu/posts/java-advanced-day03/
作者
RK
发布于
2026-08-07
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
RK
Long may the sunshine
公告
Long may the sunshine
分类
标签
最新动态
站点天气
天气加载中…
站点统计
文章
27
分类
6
标签
59
总字数
39,139
篝火已燃
0 天
最后活动
0 天前
每日一签