Java I/O — 输入输出全面笔记

从底层字节流到高层缓冲流,从文件操作到网络通信

InputStream/OutputStream Reader/Writer Buffered 缓冲 Socket 网络 try-with-resources

目录导航(点击跳转)

一、输入输出基本概念

1 核心定义

I/O 的本质是数据在不同介质之间的流动。理解 I/O 的关键在于确定以内存为参照点

  • 输入(Input):外部内存中读取数据
  • 输出(Output):内存外部写入数据
2 数据流向图
外部世界
文件 网络 键盘
输入
Input
内存(程序)
你的程序
输出
Output
外设
显示器 文件 网络

记忆口诀:外→内为输入,内→外为输出。永远站在内存的视角看问题。

流向 操作 方法示例
外部 → 内存 输入(Input) read()
内存 → 外部 输出(Output) write()

二、File 类与文件创建

1 File 对象 ≠ 物理文件

关键认知:File file = new File(path) 并不会在磁盘上创建一个真实的文件!它只是在内存中创建了一个表示路径的对象

当你把它作为参数传给 FileInputStream时,如果文件不存在就会创建出来。

File file = new File("test.txt");      // 只是内存中的一个路径对象,不是物理文件
FileInputStream fis = new FileInputStream(file);  // 此时如果文件不存在,则创建物理文件

三、字节流:InputStream / OutputStream

1 基本用法

FileInputStreamFileOutputStream是 Java 中最基础的字节流,用于按字节读写文件。传路径或传File 对象都可以。

// 方式一:传路径字符串
InputStream inputStream = new FileInputStream("path/to/file.txt");
OutputStream outputStream = new FileOutputStream("path/to/file.txt");

// 方式二:传 File 对象
File file = new File("path/to/file.txt");
InputStream inputStream = new FileInputStream(file);
OutputStream outputStream = new FileOutputStream(file);
2 流式继承关系图
Closeable(接口:定义 close() 资源释放规则)
InputStream(抽象类:定义 read() 字节读取规范)
↑ ↑ ↑
│ │ │
FileInputStream ByteArrayInputStream BufferedInputStream

字节流体系的核心:InputStream 是抽象类,定义了 read()规范;具体实现类决定从哪里读(文件、字节数组、网络等)。

3 try-with-resources 自动管理资源

使用 try块来自动管理资源,无需手动调用 close()

try (InputStream inputStream = new FileInputStream(file)) {
    // 使用 inputStream 读取数据
    int data = inputStream.read();
    // ...
} catch (IOException e) {
    e.printStackTrace();
}
// try 块结束时,inputStream.close() 会被自动调用

这是用来读字节的。InputStream 以 byte为单位操作数据,是最底层的读取方式。

四、字符流:Reader / Writer

1 在字节流之上加一层 Reader

InputStream 只能读字节,Reader 才能读字符。在字节流之上再套一个 InputStreamReader来读取字符:

// 通用方式(推荐):InputStreamReader 可以适配任何 InputStream
Reader reader = new InputStreamReader(inputStream);
// 不限于文件 — 网络流、内存流等任何 InputStream 都可以转成 Reader

当然也可以直接读文件:

// 便捷方式:直接从文件读取字符
Reader reader = new FileReader("path/to/file.txt");
2 InputStreamReader 的设计思想
任何数据源
文件 / 网络 / 内存
InputStream
字节流
InputStreamReader
字节→字符 桥梁
Reader
字符流
字符串 / String
产生字节 套一层解码 产生字符 你的程序消费

这个设计相对更通用— 因为你并不是总是从文件里面读取数据,网络、内存、管道等各种来源都可以通过 InputStreamInputStreamReader这条路径处理。

3 字节流 vs 字符流对比
特性 字节流 字符流
处理单位 byte(8位) char(16位)
适用场景 二进制文件(图片、音视频、压缩包) 文本文件(.txt、.java、.xml)
编码处理 不处理编码 自动处理字符编码
抽象基类 InputStream/ OutputStream Reader/ Writer
文件操作类 FileInputStream/ FileOutputStream FileReader/ FileWriter
桥梁转换 - InputStreamReader/ OutputStreamWriter

选择原则:处理文本用字符流,处理二进制数据用字节流。当不确定时,用字节流总是安全的。

五、缓冲流:BufferedReader / BufferedWriter

1 在 Reader 之上再来一层 BufferedReader

Reader之上再包一层 BufferedReader,实现整行读取

BufferedReader bufferedReader = new BufferedReader(reader);
String line = bufferedReader.readLine();  // 整行整行地读
2 缓冲区的核心原理 — 仓库比喻

BufferedReader 的目的其实是一次性缓存更多字节,避免每次都从文件里面操作,减少磁盘 I/O 开销

无缓冲:每次都要去超市

程序
每次都去
超市
磁盘文件
每次返回一件
超市
有缓冲:建一个仓库

程序
要东西
仓库
Buffer 8KB
仓库
仓库空了才去
超市
磁盘文件
超市
一次拉一车(8KB)
仓库
仓库
直接从仓库拿

核心机制:就好像从超市拿东西,每次家里一需要就去超市拿,一来一回很不方便。但如果建了一个仓库,一次性多存一点,要的时候直接去仓库拿就行了。BufferedReader 里有个私有变量 private static int defaultCharBufferSize = 8192,也就是默认一次性读 8KB 个字节

3 写入端:BufferedWriter / BufferedOutputStream

写的时候也可以用 Buffer:

BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
bufferedOutputStream.write(data);  // 并不会直接写入文件!
bufferedOutputStream.flush();      // 主动将缓冲区字节冲入文件

关键细节:调用 bufferedOutputStream.write()并不会直接写入文件,数据先存在缓冲区,直到缓冲区满了才会真正写入文件。你需要主动调用 flush()将缓冲区字节冲入文件。

设计目的:主要是为了把多次的 I/O 操作合并成一次,减少磁盘操作次数,大幅提升性能。

自动 flush:BufferedOutputStream 在关闭(close)的时候也会自动把缓冲区的字节冲入文件,所以即使忘记调用 flush(),只要正常关闭了流,数据也不会丢失。

4 标准套缓冲流写法
// 原始流
InputStream in = ...;
// 套缓冲流(标准写法 — 有利无害)
BufferedInputStream bin = new BufferedInputStream(in);
// 再用 bin.read(buffer) 读
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = bin.read(buffer)) != -1) {
    // 处理读取到的数据
}

这种写法有利无害。即使数据量小,套一层 Buffered 也不会引入额外问题;数据量大时,性能提升非常明显。

5 缓冲区大小选择指南
缓冲区大小 适用场景 说明
4KB 小文件、频繁读写 内存占用小,适合频繁操作
8KB(默认) 通用场景 平衡性能与内存,推荐默认使用
16KB-64KB 大文件复制、批量处理 减少 I/O 次数,提升吞吐量
128KB+ 超大文件、顺序读写 配合磁盘预读,最大化吞吐量

注意:缓冲区不是越大越好。过大的缓冲区会增加内存占用,且可能无法被 CPU 缓存有效利用。通常 8KB-32KB 是最优范围。

6 缓冲流体系完整继承图
Closeable(接口)
Reader(抽象类:字符读取)
BufferedReader(加缓冲 + readLine())
你的代码

同理,OutputStream → BufferedOutputStream、Writer → BufferedWriter 也是一样的层次结构。

7 完整装饰链一览
读取链路
数据源
文件/网络/内存
FileInputStream
字节流
BufferedInputStream
加字节缓冲
BufferedInputStream
InputStreamReader
字节→字符
BufferedReader
加字符缓冲
readLine()
一行一行读
写入链路
write()
写入数据
BufferedWriter
加字符缓冲
OutputStreamWriter
字符→字节
OutputStreamWriter
BufferedOutputStream
加字节缓冲
FileOutputStream
字节流
目标文件

六、Closeable 接口与资源管理

1 什么是"关闭文件"?

文件的"关闭"其实是内存的释放

当你读写文件时,系统会腾出一块内存来存储文件的相关信息(文件描述符、缓冲区等),关闭文件 = 释放这块内存

程序 内存(JVM) 磁盘文件
1 打开文件 → 分配缓冲区
2 读取数据到缓冲区
3 返回数据
4 从缓冲区读取
文件一直保持打开状态
5 close()→ 释放缓冲区
6 断开文件连接
内存已释放
2 Closeable 接口体系

因为 InputStreamOutputStream都实现了 Closeable接口,且 Reader也实现了 CloseableBufferedReader又继承自 Reader,所以它们都可以通过 try 来自动管理资源

Closeable extends AutoCloseable(接口:定义 close())
↑ ↑ ↑ ↑
│ │ │ │
InputStream OutputStream Reader Writer
↑ ↑
│ │
BufferedReader BufferedWriter
// 所有实现了 Closeable 的都可以这样用
try (InputStream in = new FileInputStream("read.txt");
     OutputStream out = new FileOutputStream("write.txt");
     BufferedReader reader = new BufferedReader(new FileReader("data.txt"));
     BufferedWriter writer = new BufferedWriter(new FileWriter("out.txt"))) {

    // 所有流都会在 try 块结束时自动关闭
    // 关闭顺序与声明顺序相反

} catch (IOException e) {
    e.printStackTrace();
}

七、文件复制

1 Android 中的便捷方法

Android 中复制文件的方法多种多样:

  • FileUtils工具类提供现成的复制方法
  • Kotlin 的 File自带扩展方法 copyTo()
2 Java NIO Files.copy() 方法(推荐)

Java 7+ 提供的 Files.copy()是最简单高效的文件复制方式:

// 最简单的复制方式
Path source = Paths.get("source.txt");
Path target = Paths.get("target.txt");
Files.copy(source, target);

// 覆盖已存在的目标文件
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

// 保留文件属性(权限、时间戳等)
Files.copy(source, target, 
    StandardCopyOption.REPLACE_EXISTING,
    StandardCopyOption.COPY_ATTRIBUTES);

// 复制目录(不递归)
Files.copy(sourceDir, targetDir);

// 递归复制目录
Files.walk(sourceDir)
    .forEach(sourcePath -> {
        Path targetPath = targetDir.resolve(sourceDir.relativize(sourcePath));
        try {
            Files.copy(sourcePath, targetPath);
        } catch (IOException e) {
            e.printStackTrace();
        }
    });

推荐理由:Files.copy()底层调用操作系统原生 API,性能最优,代码简洁。

3 纯 Java 实现:字节搬运

纯 Java 就是一个字节一个字节地搬。其实那些复制文件的底层 API 也是一个一个字节照搬的,只是它们做了更多封装和优化。

// 纯 Java 文件复制 — 带缓冲的字节搬运
try (FileInputStream in = new FileInputStream("source.txt");
     FileOutputStream out = new FileOutputStream("target.txt")) {

    byte[] buffer = new byte[8192];  // 一次搬 8KB
    int bytesRead;
    while ((bytesRead = in.read(buffer)) != -1) {
        out.write(buffer, 0, bytesRead);  // 读到多少写多少
    }

} catch (IOException e) {
    e.printStackTrace();
}

本质:所有文件复制操作的底层都是字节的搬运,区别只在于缓冲区大小是否用了更底层的系统调用

4 文件复制方法对比
方法 代码复杂度 性能 适用场景
Files.copy() 最高(系统调用) 通用场景,推荐首选
FileUtils.copy() Android 项目,需要额外依赖
Kotlin File.copyTo() Kotlin 项目
手动字节流 需要自定义处理逻辑
BufferedInputStream/OutputStream 较高 需要缓冲控制

八、Socket 网络通信

1 用 Socket 与网络交互

Socket 的本质就是把网络连接也当成一个流来读写。和文件操作一样,你拿到的是 InputStreamOutputStream

try {
    Socket socket = new Socket("hencoder.com", 80);

    // 往网络输出(发送数据)
    OutputStream out = socket.getOutputStream();

    // 从网络读入(接收数据)
    InputStream in = socket.getInputStream();

} catch (IOException e) {
    e.printStackTrace();
}
2 Socket 也套 Buffered:按字符读写

其实套一层 Buffered 更好:

try {
    Socket socket = new Socket("hencoder.com", 80);

    // 用 BufferedWriter 按字符写入
    BufferedWriter writer = new BufferedWriter(
        new OutputStreamWriter(socket.getOutputStream())
    );

    // 用 BufferedReader 按字符读取
    BufferedReader reader = new BufferedReader(
        new InputStreamReader(socket.getInputStream())
    );

    // reader 和 writer 是根据【字符】来写入和读取的,而不是字节

} catch (IOException e) {
    e.printStackTrace();
}

关键区别:ReaderWriter是根据字符来写入和读取的,而不是字节。发送文本数据时,按字符处理远比按字节方便。

3 发送 HTTP 报文示例
// 发送 HTTP GET 请求报文
writer.write("GET / HTTP/1.1\n"
           + "Host: www.example.com\n"
           + "\n");  // 空行表示请求头结束

writer.flush();  // 一定要 flush!否则数据还在缓冲区

// 读取服务器响应
System.out.println(reader.readLine());
4 Socket 通信全流程
客户端 服务器
1 new Socket("host", 80) — 建立 TCP 连接
2 连接成功
客户端获取 OutputStream,准备发送数据
3 writer.write(报文) + writer.flush()
数据通过网络传输
4 服务器返回响应
客户端获取 InputStream,reader.readLine()
5 socket.close() — 断开连接

九、ServerSocket 服务端

1 创建服务器:监听端口,接受连接
ServerSocket serverSocket = new ServerSocket(80);  // 监听 80 端口
Socket socket = serverSocket.accept();              // 阻塞等待客户端连接

// 别人连接你之后,你会得到一个 Socket 对象
// 然后可以调用这个 socket 对象来给对方发送消息

一样可以套一层 BufferedReaderBufferedWriter

BufferedReader reader = new BufferedReader(
    new InputStreamReader(socket.getInputStream())
);
BufferedWriter writer = new BufferedWriter(
    new OutputStreamWriter(socket.getOutputStream())
);
2 客户端-服务端交互模型
服务端
1 ServerSocket(80)
2 .accept() 阻塞等待
3 得到 Socket 对象
getInputStream → BufferedReader读取客户端请求
getOutputStream → BufferedWriter发送响应
发送请求
返回响应
客户端
1 new Socket("host", 80)
getOutputStream → BufferedWriter发送请求
getInputStream → BufferedReader读取响应

十、完整示例:HTTP 服务器

1 运行以下方法然后打开浏览器访问 http://localhost:8080 即可
private static void io3() {
    try (
        ServerSocket serverSocket = new ServerSocket(8080);
        Socket socket = serverSocket.accept();
        BufferedReader bufferedReader = new BufferedReader(
            new InputStreamReader(socket.getInputStream())
        );
        BufferedWriter bufferedWriter = new BufferedWriter(
            new OutputStreamWriter(socket.getOutputStream())
        );
    ) {

        String html = """
            <!DOCTYPE html>
            <html>
            <head>
                <meta charset="UTF-8">
                <title>简单页面</title>
            </head>
            <body>
                <h1>Hello, World!</h1>
                <p>这是一个简单的HTML页面</p>
            </body>
            </html>
            """;

        bufferedWriter.write("HTTP/1.1 200 OK\r\n");
        bufferedWriter.write("Content-Type: text/html; charset=UTF-8\r\n");
        bufferedWriter.write("Content-Length: " + html.getBytes().length + "\r\n");
        bufferedWriter.write("\r\n");
        bufferedWriter.write(html);
        bufferedWriter.flush();

    } catch (IOException _) {}
}
2 代码逐段解析
1
ServerSocket(8080)
在 8080 端口监听
2
.accept()
阻塞等待连接
3
获取 BufferedReader
读取 HTTP 请求
+
4
获取 BufferedWriter
准备发送响应
5
构造 HTML 内容
6
写入 HTTP 响应头
200 OK + Content-Type
+ Content-Length
7
写入空行
头与体分隔
8
写入 HTML 正文
9
flush()
真正发送给浏览器
try 块结束
自动关闭所有资源
步骤 关键点 说明
HTTP 响应格式 状态行 → 响应头 → 空行 → 响应体 HTTP 协议规定格式,不能错序
Content-Length html.getBytes().length 必须用字节长度,因为网络传输按字节
flush() 关键步骤 没有 flush,浏览器会一直在等待数据
try-with-resources 4层嵌套资源 全部自动关闭,顺序:writer → reader → socket → serverSocket

十一、核心思想总结

Java I/O 核心要点

1 I/O 操作最佳实践
操作场景 推荐做法 避免做法
文本读取 BufferedReader + try-with-resources 逐字符读取,不使用缓冲
文本写入 BufferedWriter + 指定编码 频繁调用 write()不使用缓冲
二进制文件 BufferedInputStream/OutputStream 使用字符流处理二进制数据
文件复制 Files.copy()(Java 7+) 手动逐字节复制
资源管理 try-with-resources 手动 try-finally 管理资源
字符编码 显式指定 StandardCharsets.UTF_8 依赖系统默认编码

Java I/O 流式设计的优点

  • 统一的抽象:文件、网络、内存都用同一套 API
  • 装饰器模式灵活组合:字节流→字符流→缓冲流,按需叠加
  • try-with-resources 自动管理,避免资源泄漏
  • 缓冲机制显著提升性能,8KB 一次搬运比逐字节快数十倍

常见陷阱

  • 忘记 flush() 导致数据留在缓冲区不写出
  • 忘记关闭流导致文件描述符泄漏
  • 不用缓冲区直接逐字节读写,性能极差
  • 读写字符时未指定编码,导致中文乱码