IO 流体系总览 | JavaSE

IO 流体系总览

一、学习目标

学完本章,你应该能够:

  1. 能够解释 IO 中 Input 和 Output 的方向是相对于 Java 程序/内存定义的。
  2. 能够解释什么是流(Stream),以及为什么 IO 可以抽象成连续的数据流动。
  3. 能够从“数据方向”和“数据单位”两个维度对 Java IO 进行分类。
  4. 能够闭卷写出 Java IO 四大抽象基类:
    • InputStream
    • OutputStream
    • Reader
    • Writer
  5. 能够区分字节流与字符流,并判断文本、图片、视频、压缩包等数据应该优先使用哪类流。
  6. 能够建立 FileInputStream/FileOutputStream/FileReader/FileWriter 的基本继承关系。
  7. 能够理解缓冲流、转换流、打印流、数据流分别在基础流之上解决什么问题。
  8. 能够解释为什么 InputStreamReader 属于字符输入体系,而 PrintStream 本质属于字节输出体系。
  9. 能够画出整个 GROUP 07 后半部分的 IO 学习路线。

二、核心知识

2.1 IO 是什么

IO:

Input
+
Output

即:

输入(Input)与输出(Output)。

Java 程序经常需要和外部世界交换数据:

文件
网络
内存
设备
其他程序

例如:

读取 Markdown 文件
保存日志
上传头像
下载图片
读取配置
复制视频
网络通信

这些本质都属于:

数据输入 / 输出

2.2 什么是 Input

Input:

数据从外部进入 Java 程序。

以 JVM/程序内存为观察中心:

外部数据源
    │
    │ Input
    ▼
Java 程序

例如:

磁盘文件
    ↓
Java 内存

属于:

Input

也就是:

读。


2.3 什么是 Output

Output:

数据从 Java 程序流向外部。

Java 程序
    │
    │ Output
    ▼
外部目标

例如:

Java 内存
    ↓
磁盘文件

属于:

Output

也就是:

写。


2.4 Input / Output 到底相对于谁

这是 IO 最重要的方向问题。

不是相对于:

文件

而是相对于:

当前 Java 程序。

因此:

文件
→
Java

叫:

输入

而:

Java
→
文件

叫:

输出

记忆方法:

以 Java 程序为中心:

进来
=
Input

出去
=
Output

2.5 为什么叫“流”

Stream 可以形象理解成:

水流

数据不是凭空瞬移:

源
↓
一个个数据单位不断流动
↓
目标

例如读取文件:

文件
│
│ byte byte byte byte ...
▼
Java

输出文件:

Java
│
│ byte byte byte byte ...
▼
文件

所以:

流(Stream)是一种连续传输数据的抽象。


2.6 IO 的第一个分类维度:方向

按照:

数据流动方向

可以分成:

输入流
Output? ×

Input
→ 输入流

Output
→ 输出流

即:

| 方向 | 作用 | | ------ | ---------------------- | | 输入流 | 从外部读取数据进入程序 | | 输出流 | 从程序写出数据到外部 |


2.7 IO 的第二个分类维度:数据单位

按照主要处理的数据单位:

字节流
字符流

2.8 什么是字节流

字节流:

byte 为基本数据单位处理 IO。

核心抽象类:

InputStream
OutputStream

因此:

字节输入流
=
InputStream

字节输出流
=
OutputStream

2.9 字节流可以处理什么

计算机所有文件最终都是:

byte

因此字节流可以处理:

文本
图片
音频
视频
PDF
ZIP
JAR
EXE
……

也就是:

原则上所有类型的数据。

特别是:

图片
视频
压缩包
可执行文件

这类二进制数据应使用:

字节流

而不能随意按字符解码。


2.10 什么是字符流

字符流:

面向字符数据进行读写。

核心抽象类:

Reader
Writer

因此:

字符输入流
=
Reader

字符输出流
=
Writer

字符流主要服务:

文本数据。

例如:

.txt
.java
.md
.json
.xml
.csv

只要这些文件:

本质上是文本内容

就可以考虑字符流。


2.11 为什么字符流主要处理文本

图片:

PNG / JPEG

虽然底层也是:

byte[]

但这些 byte 并不是:

某种字符编码得到的普通文本。

如果强行:

byte[]
↓
字符集解码
↓
char

可能破坏原数据。

所以:

二进制文件
→ 字节流

而:

文本文件
→ 字节流也能处理
→ 但字符流更符合文本语义

2.12 两个分类维度组合

现在:

方向
×
数据单位

组合起来:

| | 输入 | 输出 | | -------- | ------------- | -------------- | | 字节 | InputStream | OutputStream | | 字符 | Reader | Writer |

这就是 Java IO 最核心的:

四大抽象基类。

必须闭卷记住:

                    Java IO

                ┌──────┴──────┐
              字节流           字符流
                │                │
        ┌───────┴───────┐ ┌────┴─────┐
       输入             输出 输入          输出
        │                │    │            │
 InputStream      OutputStream Reader      Writer

2.13 四大抽象基类

InputStream

java.io.InputStream

定位:

所有字节输入流的重要抽象父类。

方向:

外部
↓
Java

单位:

byte

OutputStream

java.io.OutputStream

定位:

所有字节输出流的重要抽象父类。

方向:

Java
↓
外部

单位:

byte

Reader

java.io.Reader

定位:

字符输入流抽象父类。

方向:

外部
↓
Java

数据语义:

character

Writer

java.io.Writer

定位:

字符输出流抽象父类。

方向:

Java
↓
外部

数据语义:

character

三、使用方法

3.1 文件 IO 的四个基础实现

当数据源或目标是:

File

基础实现类:

字节:

InputStream
    ↑
FileInputStream

OutputStream
    ↑
FileOutputStream

字符:

Reader
   ↑
FileReader

Writer
   ↑
FileWriter

因此:

| 需求 | 基础流 | | ------------------ | ------------------ | | 从文件读取字节 | FileInputStream | | 向文件写字节 | FileOutputStream | | 从文本文件读取字符 | FileReader | | 向文本文件写字符 | FileWriter |

具体 API 会分别放到:

07-08 字节输入流与字节输出流

07-09 字符输入流与字符输出流

3.2 如何选择字节流还是字符流

最简单判断:

数据是不是纯文本?

如果:

不是

例如:

.jpg
.png
.mp3
.mp4
.zip
.pdf
.class

优先:

字节流

如果:

是文本

例如:

.txt
.java
.md
.json
.xml

通常:

字符流

更加自然。


3.3 不要按照文件后缀机械判断

注意:

.txt

通常是文本。

但真正判断依据不是:

扩展名

而是:

数据内容是否按照字符编码表示文本。

例如某人把:

JPEG 二进制内容

改名为:

photo.txt

它依然不是文本文件。

所以:

后缀

只是习惯。

真正依据:

数据格式

3.4 为什么文本也可以使用字节流

因为文本文件最终也是:

byte[]

所以:

FileInputStream

当然可以读取:

hello.txt

只是读取出来首先是:

byte

如果想得到:

字符

就需要:

正确解码

这正好连接上一章:

byte[]
↓
Charset
↓
String

因此:

字节流

负责:

运输 byte

字符流则进一步帮助:

按照字符语义处理文本

四、原理与进阶

4.1 Java IO 不只有文件

原课程以:

读写文件

为主线,这是 JavaSE 入门最常见场景。

但从 API 设计看:

Stream

并不等于:

File

InputStream 的数据源可以是:

文件
内存
网络
其他流

OutputStream 的目标也可以是:

文件
内存
网络
其他目的地

所以更准确:

File IO 是 Java IO 的一种重要场景,而不是 IO 的全部。


4.2 数据源与数据汇

可以建立两个术语:

Source
数据源

例如:

文件
网络连接
内存 byte[]

以及:

Sink
数据目标

例如:

文件
网络
内存

于是:

Source
   │
   │ Input
   ▼
Java
   │
   │ Output
   ▼
Sink

这就是 IO 的统一模型。


4.3 基础流和功能增强流

为了学习方便,可以把很多 IO 类理解成两类。

直接连接数据源/目标

例如:

FileInputStream
FileOutputStream
FileReader
FileWriter

可以理解为:

基础数据通道。


在已有流上增加能力

例如:

BufferedInputStream
BufferedOutputStream
BufferedReader
BufferedWriter

增加:

缓冲能力

还有:

DataInputStream
DataOutputStream

增加:

按 Java 基本数据类型读写

以及:

PrintStream
PrintWriter

增加:

方便打印各种数据

因此 IO 中经常出现:

基础流
↓
包装 / 增强
↓
获得更多能力

这种组合思想。


4.4 Java IO 的正确体系图

按照 JDK 21 类型层级,可建立如下主线:

java.io

字节输入
InputStream
├── FileInputStream
├── BufferedInputStream
├── DataInputStream
└── ...

字节输出
OutputStream
├── FileOutputStream
├── BufferedOutputStream
├── DataOutputStream
├── PrintStream
└── ...

字符输入
Reader
├── InputStreamReader
│   └── FileReader
├── BufferedReader
└── ...

字符输出
Writer
├── OutputStreamWriter
│   └── FileWriter
├── BufferedWriter
├── PrintWriter
└── ...

注意:

这张图只展示本课程相关的主要类,不是 java.io 全部类。


4.5 InputStreamReader 到底属于哪边

InputStreamReader

继承 Reader

所以它属于:

字符输入流体系

但是它的特殊职责是:

InputStream
提供 byte
      ↓
InputStreamReader
使用 Charset 解码
      ↓
提供 character

因此它被称为:

字节流到字符流的桥梁。

这正是:

07-11 转换流

要系统学习的内容。


4.6 OutputStreamWriter

方向正好相反:

character
↓
OutputStreamWriter
使用 Charset 编码
↓
byte
↓
OutputStream

因此:

OutputStreamWriter 是字符流到字节流的桥梁。


4.7 字符流底层为什么仍然离不开字节

因为:

磁盘
网络

最终通常保存或传输:

byte

所以字符输入的真正过程可以理解为:

文件 byte
↓
读取
↓
按照 Charset 解码
↓
character
↓
Java

字符输出:

Java character
↓
按照 Charset 编码
↓
byte
↓
文件

所以:

07-06 编码解码

并不是和 IO 无关的知识。

恰恰相反:

它是理解字符 IO 的理论基础。


4.8 PrintStream 到底属于字节流还是字符流

虽然名字:

PrintStream

经常用于:

System.out

输出文字,

但其继承体系是:

OutputStream
↑
FilterOutputStream
↑
PrintStream

所以:

PrintStream 本质属于字节输出流体系。

而:

PrintWriter

继承:

Writer

属于:

字符输出体系。

这两个名字非常容易混淆。


4.9 IO 为什么存在如此多的类

初学者看到:

InputStream
FileInputStream
BufferedInputStream
DataInputStream

Reader
FileReader
BufferedReader
InputStreamReader

OutputStream
FileOutputStream
BufferedOutputStream
PrintStream

Writer
FileWriter
BufferedWriter
PrintWriter

容易产生:

“Java 为什么搞这么复杂?”

实际上每个类解决不同维度的问题:

数据方向:
读 / 写

数据单位:
byte / character

数据来源:
File / memory / network

功能:
buffer / convert / print / data

可以想象成:

方向
×
数据单位
×
数据来源
×
增强功能

组合出不同的 IO 类。

所以真正应该掌握:

分类坐标系。

而不是死背几十个类。


五、实践应用

5.1 图片复制应该选什么

图片:

avatar.png

不是文本。

应该:

字节输入流
+
字节输出流

即基础思路:

PNG File
↓
FileInputStream
↓
byte
↓
FileOutputStream
↓
目标 PNG File

5.2 Markdown 文件阅读

例如:

JavaSE.md

属于文本。

如果目的是:

按字符
按行
处理文本

通常字符流更加方便:

Reader

体系。


5.3 视频文件为什么不能使用 Reader

视频:

video.mp4

底层 byte 并不是:

UTF-8 字符序列

如果错误地:

byte
↓
Charset 解码
↓
char

再写回:

原始二进制结构可能被破坏

因此:

二进制文件
=
字节流

是必须建立的基本原则。


5.4 星雨笔录中的 IO 场景

例如:

教程 Markdown
博客正文文件
日志
图片
头像
附件
备份文件

可以分类:

Markdown / 日志
→ 文本
→ 字符流较自然

PNG / JPG / ZIP / PDF
→ 二进制
→ 字节流

以后如果迁移到:

OSS
网络传输
HTTP

底层仍然离不开:

byte

的输入输出模型。


5.5 GROUP 07 后半部分完整路线

从现在开始,后面章节其实就是不断给 IO 坐标系增加能力:

07-07
IO 总览
    ↓
07-08
字节输入 / 输出
    ↓
07-09
字符输入 / 输出
    ↓
07-10
缓冲
    ↓
07-11
字节 ↔ 字符转换
    ↓
07-12
打印 / 基本数据类型
    ↓
07-13
资源释放
    ↓
07-14
Commons IO + 综合应用

所以从本章开始:

不要再孤立地背每一个类。

每遇到一个新 IO 类,都问:

它是输入还是输出?

处理 byte 还是 character?

直接连接什么数据源?

在已有流上增加什么能力?

六、常见问题

6.1 Input 是文件输入到哪里?

不是:

Java → File

而是:

File → Java

以:

Java 程序

为参照物。


6.2 Output 是输出到屏幕吗?

不一定。

Output 只表示:

数据从 Java 程序流出。

目标可能:

文件
网络
内存
控制台

6.3 InputStream 与 Reader 有什么核心区别?

InputStream
→ byte

Reader
→ character

都属于:

Input

但是数据单位和语义不同。


6.4 OutputStream 与 Writer 呢?

OutputStream
→ byte 输出

Writer
→ character 输出

6.5 字节流是不是只能操作图片和视频?

不是。

字节流:

可以操作所有基于字节的数据。

包括:

文本
图片
视频

只是文本如果需要:

按字符语义处理

字符流更方便。


6.6 字符流是不是“比字节流高级”?

不是。

二者解决:

不同的数据语义

图片使用:

Reader

反而是错误选型。

所以不是:

谁高级

而是:

谁适合当前数据

6.7 File 和 IO Stream 有什么关系?

上一阶段:

File

主要解决:

文件在哪里?
文件是什么?
目录里面有什么?

IO Stream:

真正负责数据读写。

因此:

File
≈ 文件系统对象 / 路径

Stream
≈ 数据传输通道

6.8 Reader 是不是直接从磁盘读取 char?

从高层 API 使用角度可以:

把它理解为读取字符

但底层存储最终仍是 byte。

字符流需要:

byte
↓
Charset 解码
↓
character

所以字符 IO 与字符编码密切相关。


6.9 PrintStream 为什么不是字符流?

因为它的继承关系是:

PrintStream
↓
FilterOutputStream
↓
OutputStream

所以:

它是字节输出流体系中的类

只是额外提供非常方便的:

print()
println()
printf()

能力。


七、练习与验收

7.1 知识问答

  1. IO 分别是什么单词?
  2. Input / Output 方向相对于谁定义?
  3. 文件到 Java 是 Input 还是 Output?
  4. Java 到文件是什么?
  5. Java IO 可以按照哪两个核心维度分类?
  6. 四大抽象基类分别是什么?
  7. InputStream 处理什么?
  8. OutputStream 处理什么?
  9. Reader 处理什么?
  10. Writer 处理什么?
  11. 字节流可以处理哪些数据?
  12. 字符流适合什么数据?
  13. 为什么图片不能随意使用字符流?
  14. 文本文件为什么也可以使用字节流?
  15. FileInputStream 属于哪个体系?
  16. FileWriter 属于哪个体系?
  17. InputStreamReader 属于哪个体系?
  18. 为什么 InputStreamReader 被称为桥梁?
  19. PrintStream 属于字节还是字符体系?
  20. IO 为什么会设计这么多不同的类?

7.2 代码阅读

判断每种需求应该优先选择哪个体系:

1. 复制 avatar.jpg

2. 读取 JavaSE.md 的文字内容

3. 复制 movie.mp4

4. 向 log.txt 写日志文字

5. 复制 app.zip

6. 读取 JSON 配置文本

只能先选择:

InputStream / OutputStream

或

Reader / Writer

暂时不用写具体代码。


7.3 手写代码

任务一:画 IO 分类图

闭卷画:

Java IO
    ↓
字节 / 字符
    ↓
输入 / 输出

最终必须出现:

InputStream
OutputStream
Reader
Writer

任务二:继承关系

写出:

InputStream
→ FileInputStream

OutputStream
→ FileOutputStream

Reader
→ FileReader

Writer
→ FileWriter

并说明它们分别解决什么需求。


任务三:技术选型

对下面数据做技术选择:

Markdown
PNG
MP4
ZIP
Java 源代码
日志
PDF
CSV

分别说明:

更适合字节流还是字符流?
为什么?

7.4 Debug

同学写出:

读取图片
→ FileReader

复制视频
→ FileWriter

读取 Markdown
→ FileInputStream 一定错误

PrintStream
→ Writer 子类

逐条判断:

正确 / 错误

并说明原因。


7.5 综合训练

画出下面的数据链路:

文本读取

UTF-8 文件
↓
磁盘 byte
↓
读取
↓
UTF-8 解码
↓
character
↓
Java String

文本写出

Java String
↓
character
↓
UTF-8 编码
↓
byte
↓
文件

然后回答:

  1. 哪一步属于 Input?
  2. 哪一步属于 Output?
  3. 哪一步涉及上一章的解码?
  4. 哪一步涉及上一章的编码?
  5. 为什么字符流和字符集知识必须放在一起理解?

7.6 本章验收

必须闭卷口述:

IO 是 Input / Output。

方向永远相对于 Java 程序:

外部 → Java
叫 Input。

Java → 外部
叫 Output。

IO 又可以按照数据单位分为:

字节流
字符流。

两个维度组合形成四大体系:

InputStream
字节输入

OutputStream
字节输出

Reader
字符输入

Writer
字符输出。

字节流可以处理所有类型的数据,
特别适合二进制文件。

字符流主要服务文本,
其底层涉及字符编码和解码。

File 主要描述文件系统对象,
IO Stream 才真正负责数据传输。

最终验收:

  • [ ] 闭卷写出四大抽象基类。
  • [ ] 能判断 Input / Output 方向。
  • [ ] 能区分 byte 与 character。
  • [ ] 能给图片、视频、文本选择正确流体系。
  • [ ] 能画基本继承关系。
  • [ ] 能解释 File 与 Stream 的区别。
  • [ ] 能解释字符流为什么涉及 Charset。
  • [ ] 能识别 InputStreamReader 的真实位置。
  • [ ] 能识别 PrintStream 的真实位置。
  • [ ] 已经形成完整 IO 分类坐标系。