系列文章目录
JavaIO流的使用和修饰器模式
文章目录
- 系列文章目录
- 前言
- 一、字节流:
- 1.FileInputStream(读取文件)
- 2.FileOutputStream(写入文件)
- 二、字符流:
- 1..基础字符流:
- 2.处理流:
- 3.对象处理流:
- 4.转换流:
- 三、修饰器模式
- 总结
前言
前面我们讲解了Java文件和IO流的基础部分。把流简单的分了一下类,但是我们还不知道具体是如何是使用的,下面我们将详细的讲解一下这些个流各自的职责是什么,简言之就是各自的使用方式。然后我还想给大家强戴一下IO流当中的修饰器模式,因为这个实际上通过封装真的太牛逼了。
我先给大家按字节流和字符流的分类方式来进行讲述:
一、字节流:
用于处理二进制数据(如图片、视频、任何文件),核心类为 InputStream
和 OutputStream
(1)FileInputStream(读取文件)
每次调用 read()
方法从磁盘读取1字节,频繁IO操作性能差。
适用场景:小文件读取或需要逐字节处理的场景。
try (InputStream in = new FileInputStream("test.jpg")) {
int byteData;
while ((byteData = in.read()) != -1) { // 每次读取1字节
// 处理字节(例如加密、校验)
System.out.print((char)byteData + " ");
}
} catch (IOException e) {
e.printStackTrace();
}
我们要注意,这样单个字节读取,如果文件当中有汉字就不行了。
所以进阶版可以用int read(byte[] b)方法来读取,这个方法底层是从该输入流中读取最多b.length字节数据到字节数组,如果读取正常,返回实际读取字节数, -1表示的是读取完毕了。但记得最后还要转换为字符串 new String(buf,0,readlen).
(2) FileOutputStream(写入文件)
注意点:若文件不存在会自动创建,若存在默认覆盖(通过构造参数可设置为追加模式)。
// 第二个参数 true 表示追加写入
try (OutputStream out = new FileOutputStream("log.txt", true)) {
String logEntry = "Error occurred at " + new Date() + "\n";
out.write(logEntry.getBytes(StandardCharsets.UTF_8)); // 显式指定编码
} catch (IOException e) {
e.printStackTrace();
}
这里还有一处细节要注意,就是这样创建,写入内容会覆盖原来的内容,但如果是这样创建的 new FileOutputStream(filepath,true),这样再写入内容就会追加到文件后面。
二、字符流
1.基础字符流:
(1)FileReader 读文件
这里循环读取使用read()是单个字符读取,使用read(buf)返回的是实际取到的字符数.
(2)FileWriter 写文件
这里面注意一定要关闭流,或者Flush才能真正的把数据写入到文件
2.处理流:
BufferedReader
和 BufferedWriter:
readLine()
可逐行读取文本。
// 读取CSV文件并解析
try (BufferedReader br = new BufferedReader(
new FileReader("data.csv"))) {
String line;
while ((line = br.readLine()) != null) {
String[] columns = line.split(",");
// 处理每一列数据
}
}
// 写入带换行的文本
try (BufferedWriter bw = new BufferedWriter(
new FileWriter("output.txt"))) {
bw.write("Line 1");
bw.newLine(); // 跨平台换行(Windows为\r\n,Linux为\n)
bw.write("Line 2");
}
像BufferedReader类中,有属性Reader,即可以封装一个节点流 (该节点流可以是任意的,只要是Reader的子类就行,这个我们下面讲修饰器模式再讲)。
details:
1.BufferedReader
和 BufferedWriter都是按照字符操作的。
2.不要去操作二进制文件(如声音,视频等)可能会造成文件损坏。
总结:
场景 | 正确流类型 | 原因 |
---|---|---|
图片、视频、EXE文件 | 字节流 | 直接处理原始字节,避免编解码干扰 |
文本文件(.txt) | 字符流 | 正确处理字符编码(如UTF-8、GBK) |
混合数据(如PDF) | 字节流 | PDF包含文本和二进制结构,需精确控制字节 |
网络传输数据 | 字节流 | 网络协议基于字节,而非字符 |
所以说字符流是“文本专用工具”,操作二进制文件就像用剪刀拧螺丝——不仅费力,还可能搞砸!
3.对象处理流:
能够将基本数据类型或者对象进行序列化和反序列化的操作。
这里我们需要注意的是如果需要让某个对象支持序列化机制,则必须让其类是可序列化的,而为了让某个类是可序列化的,该类必须实现如下两个接口之一:
Serializable 和 Externalizable 我们常用的是Serializable接口,因为它不用再重写方法了。
class User implements Serializable {
private static final long serialVersionUID = 1L; // 版本号
private String name;
private transient String password; // transient字段不会被序列化
}
// 序列化对象到文件
User user = new User("Alice", "secret");
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("user.dat"))) {
oos.writeObject(user);
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("user.dat"))) {
User restoredUser = (User) ois.readObject();
System.out.println(restoredUser.getName()); // 输出 "Alice"
System.out.println(restoredUser.getPassword()); // 输出 null(transient字段)
}
注意读取(反序列化)的顺序需要和保存数据(序列化)的顺序一致,否则会出现异常。
还有最容易忽略的一点就是序列化对象时,要求里面的属性的类型也需要实现序列化接口。
序列化对象时,默认将里面所有属性都会进序列化,除了static或者transient修饰的成员。
4.转换流:
乱码的本质是 字符编码不匹配:
- 写入时:文本按编码A(如UTF-8)转换为字节。
- 读取时:字节按编码B(如GBK)解码为字符。
- 结果:编码A和编码B的映射关系不同,导致字符显示错误。
转换流的作用
类名 | 功能 | 核心价值 |
---|---|---|
InputStreamReader | 将字节流(InputStream )按指定编码转换为字符流 | 解决读取时的编码问题 |
OutputStreamWriter | 将字符流按指定编码转换为字节流(OutputStream ) | 解决写入时的编码问题 |
try (Reader reader = new InputStreamReader(
new FileInputStream("utf8_file.txt"), StandardCharsets.UTF_8)) {
// 正确读取中文字符
int data;
while ((data = reader.read()) != -1) {
System.out.print((char) data);
}
}
try (Reader reader = new InputStreamReader(
new FileInputStream("utf8_file.txt"), StandardCharsets.UTF_8)) {
// 正确读取中文字符
int data;
while ((data = reader.read()) != -1) {
System.out.print((char) data);
}
}
综上可知,学习IO流我们必须要知道什么时候使用什么流。
三、修饰器模式:
其实我在学习的过程中也很疑惑这个修饰器模式到底有什么用,不就是像套娃一样一层套着一层吗,但是当我们真正理解了才发现Java设计者有多牛逼。
像以BufferedInputStream举例:
BufferedInputStream
的缓冲机制
- 内部缓冲区:
BufferedInputStream
维护一个字节数组(默认大小 8KB),用于临时存储从底层流读取的数据。 - 读取逻辑:
- 当用户调用
read()
时,BufferedInputStream
会优先从缓冲区读取数据。 - 如果缓冲区为空,它会一次性从底层
InputStream
(如FileInputStream
)读取一批数据(填满缓冲区)。 - 后续的
read()
直接从缓冲区返回数据,直到缓冲区耗尽,再重复步骤 2。
- 当用户调用
- 数据来源:
BufferedInputStream
本身不连接任何数据源(如文件、网络等),它只是一个“功能增强包装器”。 - 依赖关系:缓冲流需要底层流提供原始数据,而
FileInputStream
是唯一能直接读取文件的节点流。
装饰器模式(Decorator Pattern)的核心思想是 动态地为对象添加功能,同时保持接口的一致性。
举一个咖啡加料的例子:
假设你经营一家咖啡店,需要灵活组合咖啡和配料(如牛奶、糖),但不想为每种组合创建子类(如 MilkSugarCoffee
、SugarCoffee
等)。装饰器模式可以完美解决这个问题
1.定义基础组件:
// 基础接口:咖啡
public interface Coffee {
double getCost();
String getDescription();
}
// 具体组件:基础咖啡
public class SimpleCoffee implements Coffee {
@Override
public double getCost() { return 2.0; }
@Override
public String getDescription() { return "基础咖啡"; }
}
2. 定义装饰器基类:
// 装饰器基类:实现 Coffee 接口,并持有一个 Coffee 对象
public abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
// 委托给被装饰的 Coffee 对象
@Override
public double getCost() { return decoratedCoffee.getCost(); }
@Override
public String getDescription() { return decoratedCoffee.getDescription(); }
}
3. 具体修饰器:牛奶或糖:
// 牛奶装饰器
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() { return super.getCost() + 0.5; }
@Override
public String getDescription() { return super.getDescription() + "+牛奶"; }
}
// 糖装饰器
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() { return super.getCost() + 0.2; }
@Override
public String getDescription() { return super.getDescription() + "+糖"; }
}
4.使用修饰器的动态组合:
public class Main {
public static void main(String[] args) {
// 基础咖啡
Coffee coffee = new SimpleCoffee();
System.out.println(cost: " + coffee.getCost() + ", desc: " + coffee.getDescription());
// 加牛奶
coffee = new MilkDecorator(coffee);
System.out.println(cost: " + coffee.getCost() + ", desc: " + coffee.getDescription());
// 再加糖
coffee = new SugarDecorator(coffee);
System.out.println(cost: " + coffee.getCost() + ", desc: " + coffee.getDescription());
}
}
而在IO流中:
- 组件接口:
InputStream
(所有输入流的基类)。 - 具体组件:
FileInputStream
(直接操作文件的节点流)。 - 装饰器基类:
FilterInputStream
(实现InputStream
,并持有InputStream
对象)。 - 具体装饰器:
BufferedInputStream
(扩展FilterInputStream
,添加缓冲功能)。
// 节点流:直接读取文件
InputStream fileStream = new FileInputStream("data.txt");
// 装饰器:添加缓冲功能
InputStream bufferedStream = new BufferedInputStream(fileStream);
// 可以继续装饰:例如添加解密功能(假设有 DecryptInputStream)
InputStream decryptedStream = new DecryptInputStream(bufferedStream);
当调用 bufferedStream.read()
时:
- 检查缓冲区:如果有数据,直接返回。
- 缓冲区为空:调用底层
fileStream.read(byte[])
批量读取数据到缓冲区。 - 返回数据:从缓冲区返回一个字节。
其实吧,处理流(如 BufferedInputStream
)需要传入 InputStream
对象的核心目的,正是为了在自己的成员方法中调用底层流的 read
方法,并在其基础上添加额外功能(如缓冲、编码转换等)。这是装饰器模式的精髓所在。
总结
以上就是今天要讲的内容,本文仅简单的讲述了IO流分类后的使用和例子,然后讲了一下修饰器模式,接下来我会一直持续更新,谢谢大家。