欢迎光临
我们一直在努力

33. 【Java】I/O流(上):字节流与字符流

摘要: 本文深入讲解Java I/O流的核心概念与实战应用,涵盖字节流(InputStream/OutputStream)和字符流(Reader/Writer)两大体系。通过具体代码示例,详细解析文件读写、缓冲流优化、字符编码处理等关键技术,帮助开发者掌握高效、安全的文件操作方式。文章还提供了字节流与字符流的选择指南、常见陷阱规避方法以及一个完整的文本文件复制工具实现,为Java文件处理打下坚实基础。

关键词: Java I/O流, 字节流, 字符流, 文件读写, 缓冲流


在之前的所有文章里,我们所有的数据都存在于“内存”中——变量、数组、集合、对象。一旦程序结束,这些数据就消失了,就像一场梦醒来什么都记不住。

但你写程序,很多时候是为了处理“真实世界”的数据:从硬盘读取一个文件、把用户输入保存到磁盘、从网络下载一张图片、上传一份日志……这些数据是持久化的,它们不随程序的结束而消失。

那么,Java 是如何与外部世界“对话”的呢?

答案就是 I/O 流(Input/Output Stream)。

I/O 是 Input/Output 的缩写。你可以把流想象成一根“管道”:数据从一端流进来(输入),或者从一端流出去(输出)。这根管道连接着你的程序和外部的数据源(文件、网络、键盘、显示器等)。

今天,我们先来认识 I/O 流的两大家族:字节流和字符流。


1. 什么是流?—— 数据就像水一样流动

在 Java 中,“流”是一个抽象概念,它代表了一个有序的、连续的数据序列。

  • 输入流(Input Stream):数据从外部“流进”程序。比如,你从文件读取数据,数据从文件流向程序。
  • 输出流(Output Stream):数据从程序“流出”到外部。比如,你把数据写入文件,数据从程序流向文件。

流的本质是一连串的字节(或字符)。你可以把它想象成水管里的水——水从源头流向目的地,你可以在中途“接”一部分水(读取),也可以往里面“加”水(写入)。

Java 的 I/O 流分为两大类:

  • 字节流(Byte Stream):以 字节(8 位) 为单位进行读写。适用于任何类型的数据(文本、图片、音频、视频等)。
  • 字符流(Character Stream):以 字符(16 位 Unicode) 为单位进行读写。专门用于处理文本数据,支持字符编码转换。
  • 它们各自有四个抽象基类:

    类别输入输出
    字节流 InputStream OutputStream
    字符流 Reader Writer

    所有具体的流类都直接或间接继承自这四个基类。


    2. 字节流 —— InputStream 和 OutputStream

    字节流是 I/O 的“通用语言”。无论什么数据(文本、图片、视频),在计算机中最终都是以字节序列存在的。所以字节流可以处理任何类型的文件。

    2.1 InputStream —— 字节输入流的鼻祖

    InputStream 是所有字节输入流的父类。它定义了读取字节的基本方法:

    方法说明
    int read() 读取一个字节(0~255),如果到达末尾返回 -1
    int read(byte[] b) 读取多个字节到数组,返回实际读取的字节数
    int read(byte[] b, int off, int len) 读取指定长度的字节到数组的指定位置
    void close() 关闭流,释放系统资源(非常重要!)

    最常用的子类是 FileInputStream,用来从文件读取字节。

    // 示例:用 FileInputStream 读取一个文件
    import java.io.FileInputStream;
    import java.io.IOException;

    public class FileInputStreamDemo {
    public static void main(String[] args) {
    FileInputStream fis = null;
    try {
    // 创建 FileInputStream,指定文件路径
    fis = new FileInputStream("test.txt");
    int data;
    // 一次读一个字节,直到返回 -1(文件末尾)
    while ((data = fis.read()) != 1) {
    // 这里把字节转成字符显示(仅适用于纯文本)
    System.out.print((char) data);
    }
    } catch (IOException e) {
    System.out.println("读取文件出错:" + e.getMessage());
    } finally {
    // 确保关闭流
    if (fis != null) {
    try {
    fis.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }
    }
    }

    2.2 OutputStream —— 字节输出流的鼻祖

    OutputStream 是所有字节输出流的父类。它定义了写入字节的基本方法:

    方法说明
    void write(int b) 写入一个字节
    void write(byte[] b) 写入整个字节数组
    void write(byte[] b, int off, int len) 写入数组的一部分
    void flush() 强制将缓冲区的数据写入底层设备
    void close() 关闭流

    最常用的子类是 FileOutputStream,用来向文件写入字节。

    // 示例:用 FileOutputStream 写入文件
    import java.io.FileOutputStream;
    import java.io.IOException;

    public class FileOutputStreamDemo {
    public static void main(String[] args) {
    String content = "Hello, I/O World!\\n这是第二行。";
    try (FileOutputStream fos = new FileOutputStream("output.txt")) {
    // 把字符串转成字节数组
    byte[] bytes = content.getBytes();
    fos.write(bytes);
    System.out.println("写入成功!");
    } catch (IOException e) {
    System.out.println("写入出错:" + e.getMessage());
    }
    // 使用 try-with-resources,自动关闭流
    }
    }

    2.3 缓冲字节流 —— BufferedInputStream / BufferedOutputStream

    直接使用 FileInputStream 或 FileOutputStream 时,每次读写都会直接操作硬盘,效率较低。为了提升性能,Java 提供了缓冲流。

    缓冲流内部维护了一个缓冲区(默认 8192 字节)。读写数据时,会先操作缓冲区,满了之后才真正读写硬盘,大幅减少了 I/O 次数。

    // 用缓冲流复制文件
    import java.io.*;

    public class BufferedCopyDemo {
    public static void main(String[] args) {
    try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("source.jpg"));
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.jpg"))) {
    byte[] buffer = new byte[1024]; // 自定义缓冲区
    int bytesRead;
    while ((bytesRead = bis.read(buffer)) != 1) {
    bos.write(buffer, 0, bytesRead);
    }
    System.out.println("文件复制完成!");
    } catch (IOException e) {
    System.out.println("复制出错:" + e.getMessage());
    }
    }
    }

    小贴士:使用缓冲流时,记得调用 flush() 确保所有数据都被写出(close() 也会自动调用 flush())。


    3. 字符流 —— Reader 和 Writer

    字节流以字节为单位,但人类阅读的是字符。如果你用字节流读取一个文本文件,然后把它转成字符显示,很可能出现乱码,因为不同的编码(GBK、UTF-8)下,一个字符可能占用 1~4 个字节。

    字符流就是为了解决这个问题而生的。它在字节流的基础上,增加了字符编码转换的能力。你只需要指定字符集,字符流就会自动完成字节 ↔ 字符的转换。

    3.1 Reader —— 字符输入流的鼻祖

    Reader 是所有字符输入流的父类。它的核心方法是 read(),但操作的是 char(字符)而不是 byte。

    最常用的子类是 FileReader,用来读取文本文件。

    // 用 FileReader 读取文本文件
    import java.io.FileReader;
    import java.io.IOException;

    public class FileReaderDemo {
    public static void main(String[] args) {
    try (FileReader fr = new FileReader("test.txt")) {
    int ch;
    while ((ch = fr.read()) != 1) {
    System.out.print((char) ch);
    }
    } catch (IOException e) {
    System.out.println("读取出错:" + e.getMessage());
    }
    }
    }

    3.2 Writer —— 字符输出流的鼻祖

    Writer 是所有字符输出流的父类。它提供了写入字符的方法。

    最常用的子类是 FileWriter,用来写入文本文件。

    // 用 FileWriter 写入文本
    import java.io.FileWriter;
    import java.io.IOException;

    public class FileWriterDemo {
    public static void main(String[] args) {
    try (FileWriter fw = new FileWriter("output.txt", true)) { // true 表示追加模式
    fw.write("这是追加的内容\\n");
    fw.write("另一行内容");
    System.out.println("写入成功!");
    } catch (IOException e) {
    System.out.println("写入出错:" + e.getMessage());
    }
    }
    }

    FileWriter 默认使用系统默认编码。如果你需要指定编码(比如 UTF-8),建议使用 OutputStreamWriter 或 Files.newBufferedWriter()。

    3.3 缓冲字符流 —— BufferedReader / BufferedWriter

    和字节流一样,字符流也有缓冲版本,而且还有额外的好处:BufferedReader 提供了 readLine() 方法,可以一次读取一整行文本,非常方便。

    // 用 BufferedReader 逐行读取
    import java.io.*;

    public class BufferedReaderDemo {
    public static void main(String[] args) {
    try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
    System.out.println("行内容:" + line);
    }
    } catch (IOException e) {
    System.out.println("读取出错:" + e.getMessage());
    }
    }
    }

    // 用 BufferedWriter 写入
    import java.io.*;

    public class BufferedWriterDemo {
    public static void main(String[] args) {
    try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
    bw.write("第一行");
    bw.newLine(); // 写入换行符(跨平台)
    bw.write("第二行");
    bw.newLine();
    bw.write("第三行");
    } catch (IOException e) {
    System.out.println("写入出错:" + e.getMessage());
    }
    }
    }


    4. 字节流 vs 字符流 —— 怎么选?

    场景推荐流类型
    读写任何类型的文件(图片、音频、视频、二进制文件) 字节流(FileInputStream / FileOutputStream)
    读写纯文本文件,并且关心字符编码 字符流(FileReader / FileWriter 或 InputStreamReader / OutputStreamWriter)
    需要逐行读取文本 BufferedReader(支持 readLine())
    需要高性能、大数据量 使用缓冲流包装基础流

    重点:不要用字节流去读取文本文件,除非你手动处理编码。字符流会帮你自动处理编码,让你的代码在不同操作系统上都能正常工作。


    5. 指定字符编码 —— 避免乱码

    在不同的操作系统和环境中,默认字符编码可能不同(Windows 是 GBK,Linux 是 UTF-8)。如果你用默认编码读写文件,换一个环境就可能出现乱码。

    解决方案:显式指定字符编码。

    使用 InputStreamReader 和 OutputStreamWriter 这两个“转换流”,它们在字节流和字符流之间搭了一座桥,允许你指定字符集。

    // 指定 UTF-8 编码读取文件
    import java.io.*;

    public class EncodingDemo {
    public static void main(String[] args) {
    try (BufferedReader br = new BufferedReader(
    new InputStreamReader(
    new FileInputStream("test.txt"),
    "UTF-8" // 指定编码
    )
    )) {
    String line;
    while ((line = br.readLine()) != null) {
    System.out.println(line);
    }
    } catch (IOException e) {
    System.out.println("读取出错:" + e.getMessage());
    }
    }
    }

    从 JDK 11 开始,Files 工具类提供了更简洁的方法:

    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    import java.util.List;

    public class FilesDemo {
    public static void main(String[] args) throws IOException {
    // 一次性读取所有行(适用于小文件)
    List<String> lines = Files.readAllLines(Paths.get("test.txt"));
    lines.forEach(System.out::println);

    // 一次性写入所有行
    List<String> content = List.of("第一行", "第二行", "第三行");
    Files.write(Paths.get("output.txt"), content);
    }
    }

    Files 类默认使用 UTF-8,大大简化了代码。我们下一篇会深入介绍 NIO 和 Files 工具类的更多用法。


    6. 常见陷阱与注意事项

    ① 忘记关闭流

    不关闭流会导致资源泄漏(文件句柄、网络连接等)。始终用 try-with-resources 来自动关闭。

    ② 在字符流中用了字节流,或者反过来

    不要混淆——字符流处理文本,字节流处理二进制。用错了会导致乱码或数据损坏。

    ③ 缓冲流忘记 flush()

    缓冲流在写入时会先把数据存在内存缓冲区,如果没有 flush() 或 close(),数据可能没有真正写入硬盘。close() 会自动调用 flush(),所以只要正常关闭就没问题。

    ④ 路径问题

    文件路径可以是绝对路径(C:/data/test.txt)或相对路径(data/test.txt)。相对路径是相对于当前工作目录(通常是项目的根目录)。在 IDE 中运行,路径可能会因为运行配置的不同而变化。建议使用绝对路径或把文件放在 src 目录下用类加载器读取。

    ⑤ 编码不一致

    读取时指定的编码与文件实际的编码不一致,会出现乱码。保持读写编码一致。


    7. 综合示例 —— 一个简单的文本文件复制工具

    我们用今天学的知识,写一个可以复制文本文件的工具,支持指定编码和追加模式:

    import java.io.*;
    import java.nio.charset.Charset;

    public class TextFileUtil {
    /**
    * 复制文本文件
    * @param srcPath 源文件路径
    * @param destPath 目标文件路径
    * @param encoding 字符编码(如 "UTF-8")
    * @param append 是否追加到目标文件末尾
    */

    public static void copyTextFile(String srcPath, String destPath,
    String encoding, boolean append) {
    // 使用 try-with-resources 自动关闭流
    try (BufferedReader reader = new BufferedReader(
    new InputStreamReader(
    new FileInputStream(srcPath),
    Charset.forName(encoding)
    ));
    BufferedWriter writer = new BufferedWriter(
    new OutputStreamWriter(
    new FileOutputStream(destPath, append),
    Charset.forName(encoding)
    )
    )) {
    String line;
    int lineCount = 0;
    while ((line = reader.readLine()) != null) {
    writer.write(line);
    writer.newLine();
    lineCount++;
    }
    System.out.printf("✅ 复制完成,共 %d 行。%n", lineCount);
    } catch (FileNotFoundException e) {
    System.out.println("❌ 文件不存在:" + srcPath);
    } catch (IOException e) {
    System.out.println("❌ IO 错误:" + e.getMessage());
    }
    }

    public static void main(String[] args) {
    // 测试:复制文件,使用 UTF-8,覆盖目标
    copyTextFile("source.txt", "destination.txt", "UTF-8", false);
    }
    }


    8. 今天的总结

    今天我们进入了 I/O 流的世界:

    • 字节流(InputStream / OutputStream):以字节为单位,可处理任何类型的数据。
    • 字符流(Reader / Writer):以字符为单位,专为文本设计,支持编码转换。
    • 缓冲流(BufferedInputStream / BufferedOutputStream / BufferedReader / BufferedWriter):提升性能,并提供了便捷方法(如 readLine())。
    • 转换流(InputStreamReader / OutputStreamWriter):在字节流和字符流之间搭桥,支持指定编码。
    • try-with-resources:自动关闭资源,防止资源泄漏。
    • 选择建议:文本用字符流,二进制用字节流;指定编码避免乱码。

    今天的内容是 I/O 的基础。你学会了如何从文件读取和写入数据。在下一篇(I/O 流下)中,我们将学习更现代的 NIO(New I/O)——它提供了更高效的文件操作(Path、Files 工具类、文件遍历等),是我们目前最推荐的现代 I/O 方式。


    动手试试

  • 写一个程序,用 FileInputStream 和 FileOutputStream 复制一张图片(二进制文件),体会字节流可以处理任何文件。

  • 用 BufferedReader 和 BufferedWriter 把一段文本按行读取,然后把每行前加上行号(如“1: 内容”),写入新文件。

  • 修改上面的 copyTextFile 方法,增加统计字符数的功能,并在复制完成后输出总字符数。

  • (挑战)写一个 FileSearch 方法,接收一个目录路径和一个关键字,遍历目录下的所有 .txt 文件,搜索包含该关键字的文件并打印文件名和匹配的行号(提示:可以用 File 类和递归)。

  • 有了 I/O 流,你的程序终于可以和外部世界“对话”了。下一篇文章,我们会走进 NIO 的世界,体验更现代、更强大的文件操作方式。

    我们下一篇见。😃

    📌 获取本系列示例代码请访问 GitCode。

    赞(0)
    未经允许不得转载:171主机测评 » 33. 【Java】I/O流(上):字节流与字符流
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址