Java基础快速入门:字节缓冲流核心原理与高效拷贝实践

本文纲要

  1. 字节流拷贝的回顾与痛点
  2. 字节缓冲流概述
  3. 缓冲流源码分析:缓冲区大小与自动关闭
  4. 缓冲流一次读写一个字节的实现与原理
    4.1 代码实现
    4.2 底层原理与内存模型
  5. 缓冲流结合字节数组的高效拷贝
    5.1 代码实现
    5.2 与单字节方式的区别
  6. 四种拷贝方式对比总结
  7. 项目代码结构
  8. 完整代码示例
  9. 小结

字节流拷贝的回顾与痛点

在学习 Java IO 的初期,我们通常使用 FileInputStreamFileOutputStream 进行文件拷贝。常见的两种基础方式为:

  1. 一次读写一个字节:每次从源文件读取一个字节,再写入目标文件。这种方式虽然简单,但每次读写都需要与硬盘交互,性能极低。
  2. 一次读写一个字节数组:手动创建一个 byte[] 数组(例如 new byte[1024]),每次读取 1024 字节,再写入目标文件。这种方式减少了与硬盘的交互次数,性能大幅提升。

Java 的设计者显然意识到了手动管理数组的麻烦,因此在标准库中提供了字节缓冲流,内置了缓冲区,让开发者无需手动创建数组即可获得高效的文件读写能力。

字节缓冲流概述

Java 中的字节缓冲流分为两种:

  • BufferedInputStream:字节缓冲输入流,用于高效读取数据。
  • BufferedOutputStream:字节缓冲输出流,用于高效写出数据。

核心特点:

  • 缓冲流本身不直接操作文件,必须依赖底层的字节流(如 FileInputStream / FileOutputStream)才能真正读写数据。
  • 缓冲流的本质是在内存中提供了一个默认长度为 8192 字节的数组,用于暂存数据,从而减少与硬盘的交互次数,大幅提升效率。
  • 当关闭缓冲流时,其关联的底层字节流也会被自动关闭。

缓冲流源码分析:缓冲区大小与自动关闭

通过查看 JDK 源码,我们可以验证缓冲区的大小和关闭行为。

1 ) 缓冲区大小

BufferedInputStream 的构造方法中,会调用另一个构造方法,内部使用 DEFAULTBUFFERSIZE 常量:

// BufferedInputStream 源码片段
private static int DEFAULTBUFFERSIZE = 8192;
 
public BufferedInputStream(InputStream in) {
    this(in, DEFAULTBUFFERSIZE);
}

在底层构造中,会创建一个长度为 size 的字节数组:

byte[] buf = new byte[size];

同样,BufferedOutputStream 的构造方法也遵循相同逻辑,底层创建长度为 8192 的字节数组。

2 ) 自动关闭底层流

close() 方法中,缓冲流会关闭其包装的底层字节流:

// BufferedInputStream 的 close() 方法 
public void close() throws IOException {
    byte[] buffer;
    while ( (buffer = buf) != null) {
        if (bufUpdater.compareAndSet(this, buffer, null)) {
            InputStream input = in;
            in = null;
            if (input != null)
                input.close();
            return;
        }
    }
}

因此,我们只需关闭缓冲流对象,底层的 FileInputStream / FileOutputStream 也会随之关闭,无需单独处理。

缓冲流一次读写一个字节的实现与原理

1 ) 代码实现

用缓冲流以单字节方式拷贝文件,代码极为简洁,与基础字节流的写法几乎一致,只是将 FileInputStream/FileOutputStream 包装在缓冲流中。

public class OutputDemo11 {
    public static void main(String[] args) throws IOException {
        // 创建字节缓冲输入流,底层自动创建长度为8192的字节数组 
        BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream("bytestream\\a.avi"));
        // 创建字节缓冲输出流,底层自动创建长度为8192的字节数组
        BufferedOutputStream bos = new BufferedOutputStream(
                new FileOutputStream("bytestream\\copy.avi"));
 
        int b;
        while ((b = bis.read()) != -1) {
            bos.write(b);
        }
 
        // 关闭缓冲流,底层字节流也会被关闭
        bis.close();
        bos.close();
    }
}

2 ) 底层原理与内存模型

尽管这里每次还是读写一个字节,但缓冲流内部有一个 8192 字节的数组作为缓冲区,其工作流程如下:

一次读取8192字节

每次读出1字节

每次写入1字节

一次写出8192字节

硬盘源文件

缓冲输入流内部数组

变量 b

缓冲输出流内部数组

硬盘目标文件

关键过程:

  1. bis.read() 底层会一次性从硬盘读取 8192 字节,填充到缓冲输入流的内部数组中。
  2. 后续调用 read() 时,实际是从内存中的数组逐字节读取,速度极快。
  3. bos.write(b) 先将字节写入缓冲输出流的内部数组,当数组满 8192 字节时,再一次性地将整个数组写入硬盘。
  4. 反复循环,直到文件拷贝完毕。

优势:将多次与硬盘的交互合并为少数几次大块传输,大幅减少内存与硬盘之间的数据交换次数,从而提升性能。

缓冲流结合字节数组的高效拷贝

1 ) 代码实现

在缓冲流的基础上,再配合自定义的字节数组,可以进一步减少 read()write() 方法的调用次数,性能更优。

public class OutputDemo12 {
    public static void main(String[] args) throws IOException {
        // 创建字节缓冲输入流
        BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream("bytestream\\a.avi"));
        // 创建字节缓冲输出流
        BufferedOutputStream bos = new BufferedOutputStream(
                new FileOutputStream("bytestream\\copy.avi"));
 
        byte[] bytes = new byte[1024];
        int len;
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, len);
        }
 
        bis.close();
        bos.close();
    }
}

2 ) 与单字节方式的区别

此时的 read(byte[])write(byte[], int, int) 是以“块”为单位进行读写,流程如下:

一次读取8192字节

每次取出1024字节

每次写入1024字节

一次写出8192字节

硬盘源文件

缓冲输入流内部数组

用户自定义数组 bytes

缓冲输出流内部数组

硬盘目标文件

区别:

  • 在单字节方式中,内存中的“倒手”是一字节一字节进行的,循环次数多。
  • 在数组方式中,每次可以倒手 1024(或用户自定义长度)个字节,循环次数大幅减少,效率更高。

四种拷贝方式对比总结

拷贝方式使用的流一次读写单位优点缺点
字节流 + 单字节FileInputStream / FileOutputStream1 字节代码简单极慢,每次读写都访问硬盘
字节流 + 字节数组FileInputStream / FileOutputStream自定义数组长度速度较快,减少硬盘交互次数需手动管理数组
缓冲流 + 单字节BufferedInputStream / BufferedOutputStream底层 8192 字节无需手动创建数组,速度介于两者之间内存倒手次数多
缓冲流 + 字节数组BufferedInputStream / BufferedOutputStream自定义数组长度 + 底层 8192 字节速度最快,开发效率高代码稍复杂

项目代码结构

本示例的项目结构如下:

bytestream/
└── src/
    └── com/
        └── wb/
            └── output/
                ├── OutputDemo11.java   // 缓冲流 - 一次读写一个字节
                └── OutputDemo12.java   // 缓冲流 - 一次读写一个字节数组

完整代码示例

OutputDemo11.java

package com.wb.output;
 
import java.io.*;
 
public class OutputDemo11 {
    public static void main(String[] args) throws IOException {
        // 利用缓冲流拷贝文件(一次读写一个字节)
 
        // 创建一个字节缓冲输入流
        // 在底层创建了一个默认长度为8192的字节数组
        BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream("bytestream\\a.avi"));
        // 创建一个字节缓冲输出流
        // 在底层也创建了一个默认长度为8192的字节数组
        BufferedOutputStream bos = new BufferedOutputStream(
                new FileOutputStream("bytestream\\copy.avi"));
 
        int b;
        while ((b = bis.read()) != -1) {
            bos.write(b);
        }
 
        // 方法的底层会把字节流也给关闭
        bis.close();
        bos.close();
    }
}

OutputDemo12.java

package com.wb.output;
 
import java.io.*;
 
public class OutputDemo12 {
    public static void main(String[] args) throws IOException {
        // 缓冲流结合数组,进行文件拷贝
 
        // 创建一个字节缓冲输入流
        BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream("bytestream\\a.avi"));
        // 创建一个字节缓冲输出流
        BufferedOutputStream bos = new BufferedOutputStream(
                new FileOutputStream("bytestream\\copy.avi"));
 
        byte[] bytes = new byte[1024];
        int len;
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, len);
        }
 
        bis.close();
        bos.close();
    }
}

小结

字节流可以操作所有类型的文件(图片、音频、视频等),但直接使用效率较低
字节缓冲流通过内置的 8192 字节数组,减少了硬盘与内存的交互次数,显著提升读写效率
缓冲流不能直接操作文件,必须包装一个基础字节流,真正干活的是底层字节流
根据需求,可选择单字节或字节数组的读写方式,其中缓冲流 + 字节数组是推荐的最高效拷贝方式
关闭缓冲流时,底层字节流会被自动关闭,无需重复关闭

掌握字节缓冲流,是 Java IO 进阶的重要一步,也是后续学习字符流、对象流等高级 IO 的基础

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值