Java IO 流为什么用装饰器模式?InputStream 家族是怎么组织的?
简化版
**Java IO 流的类库是「装饰器模式(Decorator Pattern)」的经典教科书案例——它把「数据从哪来/到哪去」和「怎么加工数据」拆成两类流,用「一层套一层」的方式自由组合。**分两类:① 节点流(源头流)——直接连数据源,负责「原始的读写」,如 FileInputStream(从文件)、ByteArrayInputStream(从内存)、SocketInputStream(从网络);② 处理流(装饰流)——套在别的流外面,给它「加功能」,如 BufferedInputStream(加缓冲)、DataInputStream(加「读基本类型」能力)、ObjectInputStream(加「读对象/反序列化」能力)。为什么用装饰器:功能可以自由组合、按需叠加——想要「带缓冲地从文件读基本类型」,就 new DataInputStream(new BufferedInputStream(new FileInputStream(f))),一层层套上去。如果不用装饰器、用继承,「文件×缓冲×数据类型×…」的组合会爆炸成无数个子类。核心:装饰器让「数据源」和「加工能力」正交组合,new A(new B(new C())) 就是在 C 上叠加 B 再叠加 A 的能力。
详细版
两类流:
| 类别 | 作用 | 例子 |
|---|---|---|
| 节点流(源头) | 直接连数据源,原始读写 | FileInputStream、ByteArrayInputStream、SocketInputStream |
| 处理流(装饰) | 套在流外面加功能 | BufferedInputStream、DataInputStream、ObjectInputStream、PushbackInputStream |
四大基类(都是装饰器的抽象组件):
| 基类 | 方向 | 单位 |
|---|---|---|
| InputStream | 输入 | 字节 |
| OutputStream | 输出 | 字节 |
| Reader | 输入 | 字符 |
| Writer | 输出 | 字符 |
// 装饰器组合:一层套一层,每层加一种能力
InputStream in =
new DataInputStream( // ③ 加「读基本类型」能力
new BufferedInputStream( // ② 加「缓冲」能力
new FileInputStream("data.bin"))); // ① 源头:从文件读字节
// 使用时,最外层的能力最丰富
DataInputStream dis = (DataInputStream) in;
int i = dis.readInt(); // 用 Data 流的能力,底层经缓冲、最终从文件读
// 字符流 + 桥接 + 缓冲的经典组合
BufferedReader reader =
new BufferedReader( // 加缓冲 + readLine() 按行读
new InputStreamReader( // 桥接:字节流→字符流(按指定编码解码)
new FileInputStream("text.txt"), StandardCharsets.UTF_8));
⚠️ 理解 Java IO 类库「乱」的钥匙,就是认出它是装饰器模式——初学者觉得「怎么这么多流类、还要一层层套,太乱了」,其实它有清晰的结构:先分「字节/字符 × 输入/输出」四个方向(四大基类),每个方向下再分「节点流(连数据源)」和「处理流(加功能)」,处理流通过构造器接收另一个流、把它包起来加功能。记住这个结构,看到
new BufferedReader(new InputStreamReader(new FileInputStream(...)))就能一眼拆解:FileInputStream是源头(字节,从文件)、InputStreamReader是桥接(字节转字符、指定编码)、BufferedReader是装饰(加缓冲和readLine)——从内到外,数据源在最里、能力在最外。
完整版教学
一、问题:功能组合的爆炸
先理解「为什么 IO 要用装饰器,而不是继承」:
IO 的需求是"多个维度自由组合":
数据源:文件 / 内存 / 网络 / 管道 ...
功能:缓冲 / 读写基本类型 / 序列化对象 / 回退 ...
如果用"继承"来实现每种组合:
BufferedFileInputStream(缓冲+文件)
DataFileInputStream(数据类型+文件)
DataBufferedFileInputStream(数据+缓冲+文件)
BufferedNetworkInputStream(缓冲+网络)
...每种组合一个子类
→ 组合爆炸!N 个数据源 × M 个功能 = N×M 个类(还不算多功能叠加)
→ 无法维护
装饰器模式的解法:
数据源和功能"正交"——分别做成类,运行时自由组合
数据源:FileInputStream、ByteArrayInputStream...
功能:BufferedInputStream、DataInputStream...(都能套在任意流外面)
组合:new Data(new Buffered(new File()))
→ N + M 个类,任意组合 → 不爆炸
装饰器要解决「功能组合的爆炸」——IO 有多个维度要自由组合(数据源 × 功能)。用继承实现每种组合会类爆炸(N 数据源 × M 功能 = N×M 个类)。装饰器让「数据源和功能正交、运行时自由组合」——数据源和功能分别做成类,new Data(new Buffered(new File())) 任意组合,只需 N+M 个类。理解「IO 要数据源×功能自由组合、继承会类爆炸、装饰器让数据源和功能正交组合(N+M 个类)」,就理解了装饰器解决的核心问题。
二、装饰器模式的结构
理解装饰器模式的通用结构,再对应到 IO:
装饰器模式的角色:
① 抽象组件(Component):定义统一接口
→ IO 里是 InputStream/OutputStream/Reader/Writer 抽象类
② 具体组件(ConcreteComponent):基础实现(数据源)
→ IO 里是 FileInputStream 等节点流
③ 抽象装饰器(Decorator):持有一个 Component,实现同样接口
→ IO 里是 FilterInputStream(处理流的父类)
④ 具体装饰器(ConcreteDecorator):加具体功能
→ IO 里是 BufferedInputStream、DataInputStream 等
关键机制:
装饰器"是"一个 Component(继承同一接口)
装饰器"有"一个 Component(构造器传入,持有引用)
→ 装饰器包着被装饰对象,调用时先做自己的事、再委托给内部对象
→ 因为都是 Component,所以能一层层套(装饰器也能被装饰)
FilterInputStream 是关键:
它是所有"处理流"的父类,内部持有 protected InputStream in
BufferedInputStream/DataInputStream 都继承它
装饰器模式的结构:抽象组件(InputStream)、具体组件(FileInputStream 等节点流)、抽象装饰器(FilterInputStream,处理流父类,持有一个 InputStream in)、具体装饰器(BufferedInputStream 等)。关键机制:装饰器「是」一个 Component(继承同接口)又「有」一个 Component(构造器传入持有引用)——包着被装饰对象,调用时先做自己的事再委托给内部对象;因为都是 Component 所以能一层层套。理解「装饰器结构:抽象组件 InputStream/具体组件节点流/抽象装饰器 FilterInputStream(持有 in)/具体装饰器、装饰器既是又有 Component 所以能层层套」,就理解了装饰器模式在 IO 里的落地。
三、四大基类与两个维度
Java IO 先按「两个维度」分出四大基类:
两个维度:
维度一:字节 vs 字符
字节流:处理原始字节(二进制、图片、任意文件)
字符流:处理字符(文本,带编码/解码)
维度二:输入 vs 输出
四大基类(抽象组件):
输入 输出
字节流 InputStream OutputStream
字符流 Reader Writer
为什么分字节和字符:
字节流:读写原始字节,不关心含义(图片、视频、任意二进制)
字符流:专门处理文本,自动处理"字节↔字符"的编码转换
→ 避免中文乱码(字符流按指定 Charset 编解码)
命名规律(记住就不乱):
XxxInputStream/XxxOutputStream → 字节流
XxxReader/XxxWriter → 字符流
Java IO 先按两个维度分四大基类:字节 vs 字符(字节流处理原始字节如图片,字符流处理文本带编解码避免乱码)、输入 vs 输出。四大基类:InputStream/OutputStream(字节)、Reader/Writer(字符)。命名规律:XxxInputStream/OutputStream 是字节流、XxxReader/Writer 是字符流。理解「IO 两维度:字节(原始字节/图片)vs 字符(文本/编解码)、输入 vs 输出、四大基类 InputStream/OutputStream/Reader/Writer、命名规律 Stream 是字节 Reader/Writer 是字符」,就理解了 IO 类库的顶层划分。
四、节点流 vs 处理流
每个方向下再分「节点流(数据源)」和「处理流(装饰)」:
节点流(具体组件,直接连数据源):
FileInputStream/FileOutputStream → 文件
ByteArrayInputStream/OutputStream → 内存字节数组
PipedInputStream/OutputStream → 管道(线程间)
(字符流的节点流:FileReader/Writer、CharArrayReader/Writer、StringReader)
→ 它们负责"原始的读写",是被装饰的基础
处理流(具体装饰器,套在流外面加功能):
BufferedInputStream/Reader → 加缓冲(减少 IO 次数)
DataInputStream/OutputStream → 加"读写基本类型"能力
ObjectInputStream/OutputStream → 加"序列化/反序列化对象"能力
PushbackInputStream → 加"回退(unread)"能力
InputStreamReader/OutputStreamWriter → 桥接(字节流↔字符流,带编码)
PrintStream/PrintWriter → 加"格式化打印"能力
判断技巧:
构造器参数是"数据源"(文件名/字节数组)→ 节点流
构造器参数是"另一个流"→ 处理流(装饰器)
每个方向下分节点流(连数据源)和处理流(装饰):节点流(FileInputStream 文件、ByteArrayInputStream 内存、PipedInputStream 管道)负责原始读写、是被装饰的基础;处理流(BufferedInputStream 缓冲、DataInputStream 读基本类型、ObjectInputStream 序列化、InputStreamReader 字节转字符桥接、PrintStream 格式化打印)套在流外加功能。判断技巧:构造器参数是数据源→节点流,是另一个流→处理流。理解「节点流连数据源(FileInputStream/ByteArray/Piped)负责原始读写、处理流套外面加功能(Buffered/Data/Object/InputStreamReader/Print)、构造器传数据源是节点流传流是处理流」,就掌握了两类流的区分。
五、组合的实战:一层层套
看几个经典组合,理解「从内到外」怎么读:
经典组合一:带缓冲地读文件里的基本类型
new DataInputStream(
new BufferedInputStream(
new FileInputStream("data.bin")))
从内到外:
FileInputStream → 源头,从文件读字节(节点流)
BufferedInputStream → 加缓冲(减少磁盘 IO)
DataInputStream → 加"readInt/readLong 等读基本类型"能力
用法:dis.readInt() → 从缓冲区读、缓冲区从文件读
经典组合二:按行读 UTF-8 文本文件
new BufferedReader(
new InputStreamReader(
new FileInputStream("text.txt"), StandardCharsets.UTF_8))
从内到外:
FileInputStream → 从文件读字节
InputStreamReader → 桥接:字节→字符(按 UTF-8 解码,防乱码)
BufferedReader → 加缓冲 + readLine()(按行读)
用法:reader.readLine() → 一行行读
读法:从内(数据源)到外(能力叠加)
最里面是"数据从哪来",一层层往外是"加什么能力"
最外层的对象,能力最全(用它调用)
实战组合「一层层套」,从内到外读:new DataInputStream(new BufferedInputStream(new FileInputStream(f)))——最里 FileInputStream(源头,从文件读字节)、中间 BufferedInputStream(加缓冲)、最外 DataInputStream(加读基本类型)。new BufferedReader(new InputStreamReader(new FileInputStream(f), UTF_8))——FileInputStream(读字节)→InputStreamReader(桥接字节转字符、按 UTF-8 解码防乱码)→BufferedReader(加缓冲+readLine按行读)。读法:最里是数据源、往外是能力叠加、最外层能力最全。理解「组合从内到外:数据源在最里、能力一层层往外叠加、最外层能力最全用它调用、InputStreamReader 是字节转字符的桥接」,就掌握了 IO 流的实战组合。
六、装饰器的优缺点与其他体现
总结装饰器的优缺点,并看它在别处的体现:
装饰器模式的优点:
✓ 灵活组合——按需叠加功能,不用为每种组合建子类(避免类爆炸)
✓ 单一职责——每个装饰器只管一种功能(缓冲的只管缓冲)
✓ 开闭原则——加新功能=加新装饰器,不改现有类
✓ 运行时组合——比继承(编译时确定)更灵活
缺点:
✗ 小对象多——一个功能一层,套多了对象多
✗ 层数多时理解成本高(要拆解 new A(new B(new C())))
✗ 调试时调用栈深(一层层委托)
装饰器模式的其他体现(不只 IO):
- Java Collections.synchronizedList/unmodifiableList(包装加同步/只读)
- Spring 的 BeanWrapper、TransactionAwareCacheDecorator
- MyBatis 的 CachingExecutor(装饰 Executor 加缓存)
- Servlet 的 HttpServletRequestWrapper
所以 IO 是学装饰器模式最好的例子——理解了 IO,就理解了装饰器
装饰器的优点:灵活组合(避免类爆炸)、单一职责(每个装饰器只管一种功能)、开闭原则(加功能=加装饰器)、运行时组合(比继承灵活);缺点:小对象多、层数多理解成本高、调用栈深。装饰器不只在 IO——Collections.synchronizedList(包装加同步)、MyBatis 的 CachingExecutor(装饰 Executor 加缓存)、Servlet 的 HttpServletRequestWrapper 等都是。IO 是学装饰器最好的例子。理解「装饰器优点(灵活组合避免类爆炸/单一职责/开闭/运行时组合)缺点(小对象多/理解成本高)、其他体现:Collections.synchronizedList/MyBatis CachingExecutor/Servlet Wrapper」,就把 IO 装饰器推广到了通用设计模式。
记忆钩子:「Java IO 是装饰器模式经典案例:先按两维度分四大基类(字节 InputStream/OutputStream vs 字符 Reader/Writer、输入 vs 输出),每个方向再分节点流(连数据源:FileInputStream/ByteArray/Piped,负责原始读写)和处理流(套外面加功能:Buffered 缓冲/Data 读基本类型/Object 序列化/InputStreamReader 桥接字节转字符带编码/Print 格式化);装饰器’既是又有’Component(FilterInputStream 是处理流父类持有 in)所以能层层套;组合从内到外 new Data(new Buffered(new File()))数据源在里能力在外;优点避免类爆炸(N+M 而非 N×M),判断:构造器传数据源是节点流、传流是处理流」。
七、常见误区与追问
- 误区:Java IO 类那么多、要套来套去是设计得乱。 是清晰的装饰器模式——先按「字节/字符 × 输入/输出」分四大基类,每个方向下分节点流(连数据源)和处理流(加功能),处理流通过构造器包住另一个流叠加能力;认出装饰器结构就不乱了。
- 误区:BufferedInputStream 是一种数据源。 不是——它是处理流(装饰器),本身不连数据源,要套在别的流外面(构造器传入另一个流)给它加缓冲能力;连数据源的是节点流(FileInputStream 等),构造器参数是数据源(文件名等)。
- 误区:字节流和字符流可以随便替换。 字节流处理原始字节(图片、二进制),字符流处理文本(带编码/解码,防乱码);读文本要用字符流或用 InputStreamReader 桥接并指定编码,直接用字节流读文本再自己转字符容易乱码。
- 误区:套的层数越多越好。 每层是一个对象、有开销,且理解和调试成本上升;应按需组合——需要缓冲加 Buffered、需要读基本类型加 Data、需要按行读文本加 BufferedReader,不需要的功能不套。
- 追问:InputStreamReader 是干什么的? 它是「字节流到字符流的桥接器」——把字节流(InputStream)转成字符流(Reader),并在转换时按指定的 Charset(如 UTF-8)解码;读文本文件时用它包住 FileInputStream 并指定编码,能正确处理中文避免乱码;OutputStreamWriter 是反方向(字符→字节,编码)。
- 追问:为什么 IO 用装饰器而不用继承来加功能? 因为 IO 的功能是多维度自由组合的(数据源 × 缓冲 × 数据类型 × 序列化…),用继承实现每种组合会类爆炸(N 数据源 × M 功能 = N×M 个子类);装饰器让数据源和功能正交、运行时自由叠加,只需 N+M 个类,符合开闭原则,加新功能只需加新装饰器。
- 追问:装饰器模式还在 Java/框架的哪里用到? Collections.synchronizedList/unmodifiableList(包装 List 加同步/只读)、MyBatis 的 CachingExecutor(装饰 Executor 加二级缓存)、Servlet 的 HttpServletRequestWrapper(包装 request 加功能)、Spring 的一些 Wrapper 类;都是「持有一个同类型对象 + 叠加功能」的装饰器套路。
八、加强记忆
Java IO 流类库是「装饰器模式」的经典案例。先按两个维度分四大基类(抽象组件):字节流(InputStream/OutputStream,处理原始字节如图片)vs 字符流(Reader/Writer,处理文本带编解码防乱码)、输入 vs 输出(命名规律:Stream 是字节、Reader/Writer 是字符)。每个方向下分两类:① 节点流(具体组件)——直接连数据源、负责原始读写(FileInputStream 文件、ByteArrayInputStream 内存、PipedInputStream 管道);② 处理流(具体装饰器)——套在别的流外面加功能(BufferedInputStream 缓冲、DataInputStream 读基本类型、ObjectInputStream 序列化、InputStreamReader 桥接字节转字符并指定编码、PrintStream 格式化)。关键机制:处理流的父类 FilterInputStream「既是 Component(继承同接口)又有 Component(构造器持有 in)」,所以能一层层套。组合从内到外:new DataInputStream(new BufferedInputStream(new FileInputStream(f)))——数据源在最里、能力一层层往外叠加、最外层能力最全。判断技巧:构造器传数据源→节点流,传另一个流→处理流。装饰器的价值是避免类爆炸(N+M 而非 N×M 个类)、单一职责、开闭原则;也用于 Collections.synchronizedList、MyBatis CachingExecutor 等。一句话「Java IO=装饰器模式:四大基类(字节 Stream/字符 Reader-Writer×输入输出),节点流连数据源(File/ByteArray)+处理流套外面加功能(Buffered/Data/Object/InputStreamReader 桥接/Print);FilterInputStream 既是又有 Component 能层层套;组合从内到外数据源在里能力在外,避免类爆炸(N+M)」。