🚀 Java 巩固进阶 · 第14天
主题:字节流 & 字符流 ------ 文件读写的核心引擎
📅 进度概览 :今天进入 IO 流的灵魂章节!掌握这 4 个核心类,你就能打通文件读写的任督二脉。
💡 核心价值:
- 万能字节流 :
InputStream/OutputStream,处理图片/视频/任意二进制文件,文件上传下载的基石。- 文本字符流 :
Reader/Writer,专为纯文本设计,自动处理编码,彻底告别中文乱码。- 资源安全 :
try-with-resources语法,自动关闭流,杜绝资源泄漏(生产环境红线!)。- 性能基石:理解"缓冲"思想,为明天学习高效流打下理论基础。
一、流的分类:一张图看懂选型逻辑 🗺️
┌─ 字节流 (Byte Stream) ──► InputStream / OutputStream
│ ├─ 万能:能读任何文件(图片/音频/视频/文本)
│ ├─ 单位:字节 (byte),8 位
│ └─ 场景:文件复制、图片上传、网络传输
│
Java IO 流 ─────┤
│ ┌─ 字符流 (Char Stream) ──► Reader / Writer
│ ├─ 专用:只能读纯文本(.txt/.java/.md/.yml)
│ ├─ 单位:字符 (char),16 位 (Unicode)
│ ├─ 核心优势:内置编码转换,防中文乱码
│ └─ 场景:配置文件读取、日志写入、模板渲染
│
└─ 🎯 选型口诀:
"一切文件皆字节,文本优先用字符,
不确定时用字节,永远不亏保平安"
🔍 本质区别:编码!编码!编码!
| 对比项 | 字节流 | 字符流 |
|---|---|---|
| 处理单位 | byte (8 位) |
char (16 位, Unicode) |
| 编码感知 | ❌ 无感知,原样读写 | ✅ 自动编解码 (默认平台编码) |
| 中文处理 | 可能乱码(需手动转码) | 自动处理,不易乱码 |
| 适用文件 | 所有文件(二进制 + 文本) | 仅纯文本文件 |
| 父类 | InputStream / OutputStream |
Reader / Writer |
⚠️ 中文乱码根源:
java// 字节流读中文:如果编码不匹配,必乱码! FileInputStream in = new FileInputStream("cn.txt"); // 文件是 UTF-8 byte[] buf = new byte[1024]; in.read(buf); String text = new String(buf); // ❌ 默认用平台编码(如GBK)解码 UTF-8 字节 → 乱码! // ✅ 正确:显式指定编码 String text = new String(buf, StandardCharsets.UTF_8); // 字符流:自动用指定编码解码(推荐!) FileReader fr = new FileReader("cn.txt"); // 默认平台编码,仍可能乱码! // ✅ 最佳:用 InputStreamReader 包装字节流,显式指定编码 InputStreamReader isr = new InputStreamReader( new FileInputStream("cn.txt"), StandardCharsets.UTF_8);
二、字节流实战:FileInputStream / FileOutputStream
1. 读取文件:三种姿势,推荐缓冲 + 数组
java
// ❌ 姿势1:单字节读取(慢!100 次 IO,不推荐)
FileInputStream in = new FileInputStream("a.txt");
int b;
while ((b = in.read()) != -1) { // 每次读 1 字节
System.out.print((char) b);
}
in.close();
// ✅ 姿势2:字节数组 + 缓冲(快!推荐⭐)
FileInputStream in = new FileInputStream("a.txt");
byte[] buf = new byte[1024 * 8]; // 8KB 缓冲,平衡内存与效率
int len;
while ((len = in.read(buf)) != -1) { // len 是实际读取字节数!
// ⚠️ 关键:必须用 0, len,避免读取上次残留数据
System.out.print(new String(buf, 0, len, StandardCharsets.UTF_8));
}
in.close();
// ✅✅ 姿势3:try-with-resources + NIO(现代写法,明天学)
2. 写入文件:覆盖写 vs 追加写
java
// 构造方法第二个参数:true=追加,false/省略=覆盖
FileOutputStream out = new FileOutputStream("log.txt", true); // ✅ 追加模式
// 写入字节数组
out.write("Hello IO\n".getBytes(StandardCharsets.UTF_8));
// 写入部分字节(配合读取时的 len 使用)
byte[] data = "Partial".getBytes();
out.write(data, 0, data.length);
out.close(); // ⚠️ 必须 close() 或 flush(),否则数据可能滞留缓冲区!
3. 🎯 文件复制:万能模板(背下来!)
java
/**
* 通用文件复制方法(支持任意文件:图片/视频/文本)
*/
public static void copyFile(File src, File dest) throws IOException {
// 1. 确保目标父目录存在
File parent = dest.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
// 2. try-with-resources 自动关流(重点!)
try (FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dest)) {
byte[] buf = new byte[1024 * 16]; // 16KB 缓冲,性能与内存平衡
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len); // ⚠️ 关键:只写实际读取的字节!
}
// ✅ 无需手动 flush()/close(),try-with-resources 自动处理
}
}
💡 为什么
out.write(buf, 0, len)而不是out.write(buf)?
假设 buf 大小=1024,最后一次读取只读了 100 字节: - buf[0~99] 是新数据,buf[100~1023] 是上次残留的旧数据 - 如果写整个 buf:会多写 924 字节垃圾数据!❌ - 正确:只写 0~len-1 的有效数据 ✅
三、字符流实战:FileReader / FileWriter(纯文本专用)
1. 读取文本:自动解码,但要注意默认编码陷阱
java
// ⚠️ 陷阱:FileReader 使用平台默认编码(Windows 通常是 GBK)
// 如果文件是 UTF-8 编码,中文可能乱码!
FileReader fr = new FileReader("cn.txt"); // 隐式用默认编码解码
int ch;
while ((ch = fr.read()) != -1) {
System.out.print((char) ch);
}
fr.close();
// ✅ 最佳实践:用 InputStreamReader 包装字节流,显式指定编码
try (InputStreamReader isr = new InputStreamReader(
new FileInputStream("cn.txt"), StandardCharsets.UTF_8)) {
char[] cbuf = new char[1024];
int len;
while ((len = isr.read(cbuf)) != -1) {
System.out.print(new String(cbuf, 0, len));
}
}
2. 写入文本:同样注意编码一致性
java
// ⚠️ FileWriter 同样使用平台默认编码
FileWriter fw = new FileWriter("out.txt", true); // 追加模式
fw.write("你好,世界!\n"); // 用平台编码编码字符
fw.close();
// ✅ 最佳:OutputStreamWriter + 显式编码
try (OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("out.txt", true), StandardCharsets.UTF_8)) {
osw.write("你好,UTF-8 编码的世界!\n");
// osw.flush(); // 可手动刷新,但 close() 会自动 flush
}
🔍 FileReader vs InputStreamReader 对比
| 特性 | FileReader | InputStreamReader + FileInputStream |
|---|---|---|
| 编码控制 | ❌ 只能用平台默认编码 | ✅ 可显式指定任意编码(UTF-8/GBK 等) |
| 跨平台 | ❌ 可能因系统编码不同导致乱码 | ✅ 编码固定,行为一致 |
| 推荐度 | ⭐⭐(仅测试/内部工具) | ⭐⭐⭐⭐⭐(生产环境首选) |
💡 SpringBoot 实践 :
读取
application.yml或模板文件时,永远显式指定编码:
java@Value("classpath:templates/email.html") private Resource template; private String readTemplate() throws IOException { try (InputStream is = template.getInputStream(); InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { return StreamUtils.copyToString(reader); // Spring 工具类 } }
四、资源管理:try-with-resources(生产环境红线!)⚠️
1. 为什么必须用?
java
// ❌ 传统写法:异常时可能漏关流 → 资源泄漏 → 线上事故!
FileInputStream in = null;
try {
in = new FileInputStream("a.txt");
// ... 业务逻辑
if (error) return; // ⚠️ 提前返回,close() 没执行!
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try { in.close(); } catch (IOException e) { e.printStackTrace(); } // 嵌套 try-catch,代码臃肿
}
}
// ✅ 现代写法:自动关流,代码简洁,异常安全
try (FileInputStream in = new FileInputStream("a.txt")) {
// ... 业务逻辑
// 无论正常返回还是抛出异常,in.close() 都会自动调用!
} catch (IOException e) {
e.printStackTrace(); // 只需处理业务异常
}
2. 使用条件 & 原理
- 条件 :资源类必须实现
java.lang.AutoCloseable接口(所有 IO 流都实现了 ✅) - 原理 :编译器自动生成
finally块,按声明逆序关闭资源 - 多资源 :用分号
;分隔,关闭顺序:后声明的先关闭
java
// 多资源示例:文件复制
try (FileInputStream in = new FileInputStream("src.txt");
FileOutputStream out = new FileOutputStream("dest.txt")) { // 先关 out,再关 in
byte[] buf = new byte[8192];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
// ✅ 无需手动 flush/close
} catch (IOException e) {
log.error("文件复制失败", e); // 生产环境用日志框架
}
💡 关闭顺序为什么重要?
- 装饰者模式流(如
BufferedInputStream包装FileInputStream)- 关闭外层流时,会自动关闭内层流
- 按逆序关闭,确保缓冲数据先刷新到底层流
五、🎯 今日实战任务:构建简易文件工具类
任务1:实现通用文件读取方法(支持编码)
java
/**
* 读取文件内容为字符串
* @param file 文件
* @param charset 字符编码(如 UTF_8)
* @return 文件内容
*/
public static String readFileToString(File file, Charset charset) throws IOException {
// TODO: 用 try-with-resources + InputStreamReader 实现
// 要求:处理文件不存在、编码异常等边界情况
}
// 测试:分别用 UTF-8 和 GBK 读取含中文的文件,观察结果
任务2:实现"安全"文件复制(带进度回调)
java
/**
* 复制大文件时,提供进度回调(SpringBoot 上传常用)
* @param src 源文件
* @param dest 目标文件
* @param callback 进度回调 (current, total) -> void
*/
public static void copyFileWithProgress(File src, File dest, ProgressCallback callback)
throws IOException {
// TODO:
// 1. 用 16KB~64KB 缓冲数组
// 2. 每复制一定字节(如 10%)调用 callback.progress(current, total)
// 3. 处理目标文件已存在的场景(覆盖/跳过/重命名)
}
// 回调接口定义
@FunctionalInterface
public interface ProgressCallback {
void progress(long copied, long total);
}
任务3:字符编码转换工具
java
/**
* 将文件从一种编码转换为另一种编码(如 GBK → UTF-8)
* @param srcFile 源文件(originalCharset 编码)
* @param destFile 目标文件(targetCharset 编码)
*/
public static void convertEncoding(File srcFile, File destFile,
Charset originalCharset, Charset targetCharset)
throws IOException {
// TODO: 用 InputStreamReader + OutputStreamWriter 实现编解码转换
// 挑战:处理 BOM 头(Byte Order Mark)
}
任务4:SpringBoot 集成小练习
yaml
# application.yml
app:
file:
upload-dir: ./uploads
default-charset: UTF-8
java
@Service
public class FileService {
@Value("${app.file.default-charset}")
private String defaultCharset;
/**
* 上传文件:保存并返回访问路径
* 要求:
* 1. 校验文件扩展名(白名单机制)
* 2. 重命名文件(UUID + 原扩展名,防覆盖)
* 3. 用字节流复制上传内容
* 4. 记录文件元数据(大小、编码、上传时间)
*/
public FileInfo upload(MultipartFile file) throws IOException {
// TODO: 实现上传逻辑
// 提示:MultipartFile.getInputStream() 返回字节流
}
}
📝 第14天 · 核心总结(极简背诵版)
-
选型决策树:
要读/写的文件是? ├─ 图片/音频/视频/任意二进制 → 字节流 (InputStream/OutputStream) ├─ 纯文本 (.txt/.java/.yml) → 字符流 (Reader/Writer) └─ 不确定 → 用字节流 + 显式编码转换,永远安全 -
编码防坑指南:
- 字节流转字符串:
new String(bytes, Charset)必须指定编码! - 字符流底层:
FileReader用平台默认编码,生产环境建议用InputStreamReader + 显式编码 - 统一项目编码:
-Dfile.encoding=UTF-8+ 编辑器保存为 UTF-8
- 字节流转字符串:
-
资源管理铁律:
- ✅ 永远用
try-with-resources自动关流 - ✅ 多资源用分号分隔,关闭顺序:后声明先关闭
- ❌ 禁止手动
close()放在try块末尾(异常时可能不执行)
- ✅ 永远用
-
高性能复制模板(背下来!):
javatry (InputStream in = new FileInputStream(src); OutputStream out = new FileOutputStream(dest)) { byte[] buf = new byte[16 * 1024]; // 16KB 缓冲 int len; while ((len = in.read(buf)) != -1) { out.write(buf, 0, len); // ⚠️ 关键:只写有效字节! } } -
SpringBoot 实践点:
- 文件上传:
MultipartFile.getInputStream()+ 字节流复制 - 配置读取:
@Value + Resource+InputStreamReader(UTF-8) - 日志写入:
FileWriter追加模式 + 自动轮转(结合 Logback)
- 文件上传: