JAVA重点基础、进阶知识及易错点总结(14)字节流 & 字符流

🚀 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天 · 核心总结(极简背诵版)

  1. 选型决策树

    复制代码
    要读/写的文件是?
    ├─ 图片/音频/视频/任意二进制 → 字节流 (InputStream/OutputStream)
    ├─ 纯文本 (.txt/.java/.yml) → 字符流 (Reader/Writer)
    └─ 不确定 → 用字节流 + 显式编码转换,永远安全
  2. 编码防坑指南

    • 字节流转字符串:new String(bytes, Charset) 必须指定编码!
    • 字符流底层:FileReader 用平台默认编码,生产环境建议用 InputStreamReader + 显式编码
    • 统一项目编码:-Dfile.encoding=UTF-8 + 编辑器保存为 UTF-8
  3. 资源管理铁律

    • ✅ 永远用 try-with-resources 自动关流
    • ✅ 多资源用分号分隔,关闭顺序:后声明先关闭
    • ❌ 禁止手动 close() 放在 try 块末尾(异常时可能不执行)
  4. 高性能复制模板(背下来!):

    java 复制代码
    try (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);  // ⚠️ 关键:只写有效字节!
        }
    }
  5. SpringBoot 实践点

    • 文件上传:MultipartFile.getInputStream() + 字节流复制
    • 配置读取:@Value + Resource + InputStreamReader(UTF-8)
    • 日志写入:FileWriter 追加模式 + 自动轮转(结合 Logback)

相关推荐
Eric.Lee20212 小时前
python实现pdf转图片png
linux·python·pdf
羊小猪~~3 小时前
Redis学习笔记(数据类型、持久化、事件、管道、发布订阅等)
开发语言·数据库·c++·redis·后端·学习·缓存
deep_drink3 小时前
1.2、Python 与编程基础:文件处理与常用库
开发语言·python·elasticsearch·llm
Hello.Reader3 小时前
一堆 `.ts` 分片合并后音画不同步?从问题定位到通用修复脚本的完整实战
python·ffmpeg·视频
结衣结衣.3 小时前
【Linux】命名管道的妙用:实现进程控制与实时字符交互
linux·运维·开发语言·学习·操作系统·交互
好家伙VCC3 小时前
**CQRS模式实战:用Go语言构建高并发读写分离架构**在现代分布式系统中,随着业务复杂度的提升和用户量的增长,传统的单数据库模型逐
java·数据库·python·架构·golang
fy121633 小时前
Java进阶——IO 流
java·开发语言·python
二妹的三爷3 小时前
Node.JS 版本管理工具 Fnm 安装及配置(Windows)
java
cngkqy3 小时前
NoClassDefFoundError: org/apache/poi/logging/PoiLogManager
java