Skip to content

传统 IO

传统 IO 通常指 java.io 包里的流式 API,例如 InputStreamOutputStreamReaderWriter。它的核心模型是:

数据像水流一样,从输入端一点点读进来,再从输出端一点点写出去。

学习目标

学完本页,你应该能说明:

  1. 字节流和字符流有什么区别。
  2. 为什么传统 IO 通常是阻塞的。
  3. 为什么要使用缓冲流。
  4. 为什么一定要关闭资源。
  5. JDK7 的 try-with-resources 解决了什么问题。
  6. 文件复制、文本读取、上传落盘这种商业场景应该怎么写。

IO 类体系

mermaid
flowchart TD
    A["Java 传统 IO"] --> B["字节流"]
    A --> C["字符流"]
    B --> D["InputStream<br/>读字节"]
    B --> E["OutputStream<br/>写字节"]
    C --> F["Reader<br/>读字符"]
    C --> G["Writer<br/>写字符"]
    D --> H["FileInputStream"]
    D --> I["BufferedInputStream"]
    E --> J["FileOutputStream"]
    E --> K["BufferedOutputStream"]
    F --> L["InputStreamReader"]
    F --> M["BufferedReader"]
    G --> N["OutputStreamWriter"]
    G --> O["BufferedWriter"]

字节流

字节流处理的是原始二进制数据。图片、PDF、压缩包、视频、网络报文,本质都是字节。

常见父类:

作用
InputStream输入字节流,负责读
OutputStream输出字节流,负责写

字符流

字符流处理的是文本。它会把字节按照某种字符集解码成字符,或者把字符编码成字节。

常见父类:

作用
Reader输入字符流,负责读字符
Writer输出字符流,负责写字符

字节流和字符流的关系:

mermaid
flowchart TD
    A["磁盘里的字节"] --> B["InputStream"]
    B --> C["InputStreamReader<br/>按 UTF-8 解码"]
    C --> D["Reader 读到字符"]
    E["Writer 写字符"] --> F["OutputStreamWriter<br/>按 UTF-8 编码"]
    F --> G["OutputStream"]
    G --> H["磁盘里的字节"]

为什么传统 IO 会阻塞

阻塞的意思是:线程发起读写后,如果数据还没准备好,线程就停在那里等。

mermaid
flowchart TD
    A["线程调用 read()"] --> B{"数据到了吗"}
    B -- "没有" --> C["线程阻塞等待"]
    C --> B
    B -- "到了" --> D["复制数据到应用缓冲区"]
    D --> E["read() 返回"]

比如服务端从 Socket 读数据:

java
int len = inputStream.read(buffer);

如果客户端一直不发数据,这个线程就可能一直卡在 read 上。文件读写也可能因为磁盘慢、网络盘慢、系统负载高而等待。

不理解阻塞会带来两个问题:

  1. 以为线程“没干活”就不占资源,其实线程栈、上下文切换、连接都在占资源。
  2. 在线程池里大量执行阻塞 IO,可能把工作线程全部占满,导致接口无响应。

装饰器模式

Java IO 的类很多,是因为它大量使用装饰器模式:基础流负责连接数据源,包装流负责增强能力。

mermaid
flowchart TD
    A["FileInputStream<br/>连接文件"] --> B["BufferedInputStream<br/>增加缓冲"]
    B --> C["InputStreamReader<br/>字节转字符"]
    C --> D["BufferedReader<br/>按行读取"]

示例:

java
try (BufferedReader reader = new BufferedReader(
        new InputStreamReader(
                new FileInputStream("orders.txt"), "UTF-8"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

这段代码里:

作用
FileInputStream从文件读字节
InputStreamReader把字节按 UTF-8 解码成字符
BufferedReader增加缓冲,并提供 readLine

JDK7:try-with-resources

JDK7 之前经常手写 finally 关闭资源,代码又长又容易漏。

JDK7 引入 try-with-resources,只要资源实现了 AutoCloseable,就可以自动关闭。

java
try (InputStream in = new FileInputStream("input.bin");
     OutputStream out = new FileOutputStream("output.bin")) {
    byte[] buffer = new byte[8192];
    int len;
    while ((len = in.read(buffer)) != -1) {
        out.write(buffer, 0, len);
    }
}

关闭顺序是后打开的先关闭。

mermaid
flowchart TD
    A["打开 FileInputStream"] --> B["打开 FileOutputStream"]
    B --> C["执行复制逻辑"]
    C --> D["关闭 FileOutputStream"]
    D --> E["关闭 FileInputStream"]

为什么必须关闭?

不关闭的资源后果
文件流文件句柄泄漏,Windows 下还可能无法删除文件
Socket连接不释放,服务端连接数耗尽
输出流缓冲区数据可能没刷到磁盘或网络
压缩流ZIP 结构可能不完整

Demo:JDK7/8 文件复制

这是最基础、最通用的写法,JDK7 和 JDK8 都能运行。

java
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class CopyFileDemo {
    public static void main(String[] args) throws IOException {
        copy("source.dat", "target.dat");
    }

    public static void copy(String source, String target) throws IOException {
        try (InputStream in = new BufferedInputStream(new FileInputStream(source));
             OutputStream out = new BufferedOutputStream(new FileOutputStream(target))) {
            byte[] buffer = new byte[8192];
            int len;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
        }
    }
}

重点不是代码长短,而是理解:

  1. read(buffer) 不保证每次读满 8192 字节。
  2. 返回 -1 表示读到末尾。
  3. write(buffer, 0, len) 必须使用本次真实读取长度。
  4. 缓冲流减少实际访问磁盘的次数。
  5. try-with-resources 确保异常时也能关闭。

Demo:JDK7 Files API

JDK7 提供了 NIO.2 的 PathFiles,日常文件操作更方便。

java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

public class FilesCopyDemo {
    public static void main(String[] args) throws IOException {
        Path source = Paths.get("source.dat");
        Path target = Paths.get("target.dat");

        Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
    }
}

Files.copy 适合普通文件复制。大文件、限速下载、断点续传、边读边校验时,仍然需要自己分块处理。

商业场景:文件上传先落临时文件

医疗数据采集、合同上传、批量导入这些场景,不建议边上传边直接覆盖正式文件。更稳妥的流程是:

mermaid
flowchart TD
    A["接收上传流"] --> B["写入临时文件"]
    B --> C["计算摘要和大小"]
    C --> D{"校验通过"}
    D -- "否" --> E["删除临时文件并返回错误"]
    D -- "是" --> F["原子移动到正式目录"]
    F --> G["写数据库记录"]

示例代码:

java
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

public class UploadSaveDemo {
    public static void save(InputStream uploadStream, String fileName) throws IOException {
        Path tempDir = Paths.get("upload-temp");
        Path finalDir = Paths.get("upload");
        Files.createDirectories(tempDir);
        Files.createDirectories(finalDir);

        Path tempFile = tempDir.resolve(fileName + ".tmp");
        Path finalFile = finalDir.resolve(fileName);

        Files.copy(uploadStream, tempFile, StandardCopyOption.REPLACE_EXISTING);

        long size = Files.size(tempFile);
        if (size <= 0) {
            Files.deleteIfExists(tempFile);
            throw new IOException("empty file");
        }

        Files.move(tempFile, finalFile, StandardCopyOption.REPLACE_EXISTING);
    }
}

这不是“多此一举”。如果直接写正式文件,上传中断时,业务线程可能读到半个文件。

常见坑

后果正确做法
不关闭流文件句柄泄漏使用 try-with-resources
复制时忽略 len目标文件多出脏数据out.write(buffer, 0, len)
文本用字节流直接转 String乱码使用 Reader 并指定编码
大文件一次性读入数组OOM分块读取
在锁里做慢 IO锁长时间不释放锁内只改内存状态,IO 放锁外
线程池里无限做阻塞 IO线程被占满控制并发、设置超时、使用有界队列

面试标准回答

传统 IO 是基于流的同步阻塞模型。字节流处理二进制数据,字符流处理文本并涉及编码解码。Java IO 常用装饰器模式,例如 FileInputStream 负责读文件,BufferedInputStream 增加缓冲,InputStreamReader 做字节到字符的转换。JDK7 的 try-with-resources 可以自动关闭实现 AutoCloseable 的资源,避免文件句柄和连接泄漏。生产中要注意缓冲、编码、大文件分块、超时和资源关闭。

关联知识点