JavaIO流的使用和修饰器模式(直击心灵版)

系列文章目录

JavaIO流的使用和修饰器模式


文章目录


前言

前面我们讲解了Java文件和IO流的基础部分。把流简单的分了一下类,但是我们还不知道具体是如何是使用的,下面我们将详细的讲解一下这些个流各自的职责是什么,简言之就是各自的使用方式。然后我还想给大家强戴一下IO流当中的修饰器模式,因为这个实际上通过封装真的太牛逼了。


我先给大家按字节流和字符流的分类方式来进行讲述:

一、字节流:

用于处理二进制数据(如图片、视频、任何文件)​ ,核心类为 InputStreamOutputStream

(1)FileInputStream(读取文件)

每次调用 read() 方法从磁盘读取1字节,频繁IO操作性能差。
适用场景:小文件读取或需要逐字节处理的场景。

java 复制代码
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(写入文件)

注意点:若文件不存在会自动创建,若存在默认覆盖(通过构造参数可设置为追加模式)。

java 复制代码
// 第二个参数 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.处理流:

BufferedReaderBufferedWriter:

readLine() 可逐行读取文本。

java 复制代码
// 读取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.BufferedReaderBufferedWriter都是按照字符操作的。

2.不要去操作二进制文件(如声音,视频等)可能会造成文件损坏。

总结:

场景 正确流类型 原因
图片、视频、EXE文件 字节流 直接处理原始字节,避免编解码干扰
文本文件(.txt) 字符流 正确处理字符编码(如UTF-8、GBK)
混合数据(如PDF) 字节流 PDF包含文本和二进制结构,需精确控制字节
网络传输数据 字节流 网络协议基于字节,而非字符

所以说字符流是"文本专用工具",操作二进制文件就像用剪刀拧螺丝------不仅费力,还可能搞砸!

3.对象处理流:

能够将基本数据类型或者对象进行序列化和反序列化的操作。

这里我们需要注意的是如果需要让某个对象支持序列化机制,则必须让其类是可序列化的,而为了让某个类是可序列化的,该类必须实现如下两个接口之一:

Serializable 和 Externalizable 我们常用的是Serializable接口,因为它不用再重写方法了。

java 复制代码
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.转换流:

乱码的本质是 ​字符编码不匹配

  1. 写入时:文本按编码A(如UTF-8)转换为字节。
  2. 读取时:字节按编码B(如GBK)解码为字符。
  3. 结果:编码A和编码B的映射关系不同,导致字符显示错误。

转换流的作用

类名 功能 核心价值
InputStreamReader 将字节流(InputStream)按指定编码转换为字符流 解决读取时的编码问题
OutputStreamWriter 将字符流按指定编码转换为字节流(OutputStream 解决写入时的编码问题
java 复制代码
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),用于临时存储从底层流读取的数据。

  • 读取逻辑

    1. 当用户调用 read() 时,BufferedInputStream优先从缓冲区读取数据
    2. 如果缓冲区为空 ,它会一次性从底层 InputStream(如 FileInputStream)读取一批数据(填满缓冲区)。
    3. 后续的 read() 直接从缓冲区返回数据,直到缓冲区耗尽,再重复步骤 2。
  • 数据来源BufferedInputStream 本身不连接任何数据源(如文件、网络等),它只是一个"功能增强包装器"。

  • 依赖关系 :缓冲流需要底层流提供原始数据,而 FileInputStream 是唯一能直接读取文件的节点流。

装饰器模式(Decorator Pattern)的核心思想是 ​动态地为对象添加功能,同时保持接口的一致性。

举一个咖啡加料的例子:

假设你经营一家咖啡店,需要灵活组合咖啡和配料(如牛奶、糖),但不想为每种组合创建子类(如 MilkSugarCoffeeSugarCoffee 等)。装饰器模式可以完美解决这个问题

1.定义基础组件:

java 复制代码
// 基础接口:咖啡
public interface Coffee {
    double getCost();
    String getDescription();
}

// 具体组件:基础咖啡
public class SimpleCoffee implements Coffee {
    @Override
    public double getCost() { return 2.0; }

    @Override
    public String getDescription() { return "基础咖啡"; }
}
  1. 定义装饰器基类:
java 复制代码
// 装饰器基类:实现 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(); }
}
  1. 具体修饰器:牛奶或糖:
java 复制代码
// 牛奶装饰器
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.使用修饰器的动态组合:

java 复制代码
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,添加缓冲功能)。
java 复制代码
// 节点流:直接读取文件
InputStream fileStream = new FileInputStream("data.txt");

// 装饰器:添加缓冲功能
InputStream bufferedStream = new BufferedInputStream(fileStream);

// 可以继续装饰:例如添加解密功能(假设有 DecryptInputStream)
InputStream decryptedStream = new DecryptInputStream(bufferedStream);

当调用 bufferedStream.read() 时:

  1. 检查缓冲区:如果有数据,直接返回。
  2. 缓冲区为空 :调用底层 fileStream.read(byte[]) 批量读取数据到缓冲区。
  3. 返回数据:从缓冲区返回一个字节。

其实吧,处理流(如 BufferedInputStream)需要传入 InputStream 对象的核心目的,正是为了在自己的成员方法中调用底层流的 read 方法,并在其基础上添加额外功能(如缓冲、编码转换等)。这是装饰器模式的精髓所在。


总结

以上就是今天要讲的内容,本文仅简单的讲述了IO流分类后的使用和例子,然后讲了一下修饰器模式,接下来我会一直持续更新,谢谢大家。

相关推荐
Lizhihao_5 分钟前
用TCP实现服务器与客户端的交互
java·服务器·开发语言
小彭努力中1 小时前
13.THREE.HemisphereLight 全面详解(含 Vue Composition 示例)
开发语言·前端·javascript·vue.js·深度学习·数码相机·ecmascript
spencer_tseng2 小时前
gradle eclipse [.project .classpath .settings]
java·ide·eclipse·gradle
VinfolHu2 小时前
【JAVA】数据类型与变量:深入理解栈内存分配(4)
java·开发语言
三思而后行,慎承诺2 小时前
Kotlin和JavaScript的对比
开发语言·javascript·kotlin
有梦想的攻城狮3 小时前
spring中的@Configuration注解详解
java·后端·spring·configuration·配置类
言之。3 小时前
Go语言中的错误处理
开发语言·后端·golang
red_redemption4 小时前
Spring Boot + MyBatis-Plus 的现代开发模式
java·spring boot·mybatis
苹果酱05674 小时前
iview内存泄漏
java·vue.js·spring boot·mysql·课程设计
Kay_Liang4 小时前
探究排序算法的奥秘(下):快速排序、归并排序、堆排序
java·数据结构·c++·python·算法·排序算法