首页 > 编程语言 >Java 缓冲流详解

Java 缓冲流详解

时间:2024-11-01 22:44:46浏览次数:3  
标签:Java 缓冲 write 详解 IOException 缓冲区 new byte

在 Java 的 I/O 体系中,缓冲流(Buffered Streams)是对字节流和字符流的一种封装,通过在内存中开辟缓冲区来提高 I/O 操作的效率。Java 提供了 BufferedInputStreamBufferedOutputStream 来实现字节流的缓冲,以及 BufferedReaderBufferedWriter 来实现字符流的缓冲。本文将详细介绍缓冲流的工作原理、使用方法以及它们在实际应用中的优势。

1 缓冲流的工作原理

缓冲流的工作原理是将数据先写入缓冲区中,当缓冲区满时再一次性写入文件或输出流,或者当缓冲区为空时一次性从文件或输入流中读取一定量的数据。这样可以减少系统的 I/O 操作次数,提高系统的 I/O 效率,从而提高程序的运行效率。

2 字节缓冲流

BufferedInputStreamBufferedOutputStream 属于字节缓冲流,它们强化了字节流 InputStreamOutputStream

2.1 构造方法

  • BufferedInputStream(InputStream in):创建一个新的缓冲输入流。
  • BufferedOutputStream(OutputStream out):创建一个新的缓冲输出流。

示例代码:

// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("b.txt"));

// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("b.txt"));

2.2 缓冲流的高效性

通过复制一个大文件来测试缓冲流的效率。首先使用基本流(非缓冲流)进行复制:

/**
 * 复制一个 220M+ 的大文件,V1:基本流实现
 * @throws IOException
 */
private static void testCopyBigFileV1() throws IOException {
    // 记录开始时间
    long start = System.currentTimeMillis();
    // 创建流对象
    try (FileInputStream fis = new FileInputStream("JavaSE/resourses/py.mp4");//exe文件够大
         FileOutputStream fos = new FileOutputStream("JavaSE/resourses/copyPy1.mp4")){
        // 读写数据
        int b;
        while ((b = fis.read()) != -1) {
            fos.write(b);
        }
    }
    // 记录结束时间
    long end = System.currentTimeMillis();
    System.out.println("普通流复制时间:"+(end - start)+" 毫秒");
}

使用缓冲流进行复制:

/**
 * 复制一个 220M+ 的大文件,V2:缓冲流复制
 * 运行结果:缓冲流复制时间:3625 毫秒
 * @throws IOException
 */
private static void testCopyBigFileV2() throws IOException {
    // 记录开始时间
    long start = System.currentTimeMillis();
    // 创建流对象
    try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("JavaSE/resourses/py.mp4"));
         BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("JavaSE/resourses/copyPy2.mp4"));){
        // 读写数据
        int b;
        while ((b = bis.read()) != -1) {
            bos.write(b);
        }
    }
    // 记录结束时间
    long end = System.currentTimeMillis();
    System.out.println("缓冲流复制时间:"+(end - start)+" 毫秒");
}

使用数组的方式进一步提高效率:

/**
 * 复制一个 220M+ 的大文件,V3:缓冲流使用数组复制
 * 运行结果:缓冲流使用数组复制时间:464 毫秒
 * @throws IOException
 */
private static void testCopyBigFileV3() throws IOException {
    // 记录开始时间
    long start = System.currentTimeMillis();
    // 创建流对象
    try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("JavaSE/resourses/py.mp4"));
         BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("JavaSE/resourses/copyPy3.mp4"));){
        // 读写数据
        int len;
        byte[] bytes = new byte[8*1024];
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 0 , len);
        }
    }
    // 记录结束时间
    long end = System.currentTimeMillis();
    System.out.println("缓冲流使用数组复制时间:"+(end - start)+" 毫秒");
}

2.3 为什么字节缓冲流会这么快?

  • 减少系统调用次数:数据先写入缓冲区,缓冲区满时再一次性写入磁盘或输出流,减少系统调用次数。
  • 减少磁盘读写次数:缓冲流会先从缓冲区中读取数据,缓冲区中没有足够的数据时再从磁盘或输入流中读取一定量的数据。
  • 提高数据传输效率:数据以块的形式进行传输,减少数据传输的次数。

2.4 源码解读

2.4.1 BufferedInputStreamread 方法

public synchronized int read() throws IOException {
    if (pos >= count) {     // 如果当前位置已经到达缓冲区末尾
        fill();             // 填充缓冲区
        if (pos >= count)   // 如果填充后仍然到达缓冲区末尾,说明已经读取完毕
            return -1;      // 返回 -1 表示已经读取完毕
    }
    return getBufIfOpen()[pos++] & 0xff; // 返回当前位置的字节,并将位置加 1
}

这段代码主要有两部分:

  • fill():该方法会将缓冲 buf 填满。
  • getBufIfOpen()[pos++] & 0xff:返回当前读取位置的字节,并将其转换为无符号整数。& 0xff 的作用是将有符号的 byte 类型转换为无符号的整数。

2.4.2 FileInputStreamread 方法

/**
* Reads a byte of data from this input stream. This method blocks
* if no input is yet available.
*
* @return     the next byte of data, or <code>-1</code> if the end of the
*             file is reached.
* @exception  IOException  if an I/O error occurs.
*/
public int read() throws IOException {
   return read0();
}

private native int read0() throws IOException;

在这段代码中,read0() 方法是一个本地方法,它的实现是由底层操作系统提供的,并不是 Java 语言实现的。在不同的操作系统上,read0() 方法的实现可能会有所不同,但是它们的功能都是相同的,都是用于读取一个字节。

2.4.3 BufferedOutputStreamwrite(byte b[], int off, int len) 方法

public synchronized void write(byte b[], int off, int len) throws IOException {
    if (len >= buf.length) {    // 如果写入的字节数大于等于缓冲区长度
        /* 如果请求的长度超过了输出缓冲区的大小,
           先刷新缓冲区,然后直接将数据写入。
           这样可以避免缓冲流级联时的问题。*/
        flushBuffer();          // 先刷新缓冲区
        out.write(b, off, len); // 直接将数据写入输出流
        return;
    }
    if (len > buf.length - count) { // 如果写入的字节数大于空余空间
        flushBuffer();              // 先刷新缓冲区
    }
    System.arraycopy(b, off, buf, count, len); // 将数据拷贝到缓冲区中
    count += len;                             // 更新计数器
}
  • flushBuffer() 方法:当缓冲区满时,flushBuffer() 方法会将缓冲区中的数据刷新到底层输出流。

  • System.arraycopy():将数据拷贝到缓冲区中,并更新计数器 count

2.4.4 FileOutputStreamwrite 方法

/**
* Writes the specified byte to this file output stream. Implements
* the <code>write</code> method of <code>OutputStream</code>.
*
* @param      b   the byte to be written.
* @exception  IOException  if an I/O error occurs.
*/
public void write(int b) throws IOException {
   write(b, append);
}

private native void write(int b, boolean append) throws IOException;
  • FileOutputStreamwrite 方法,同样是本地方法,一次只能写入一个字节。

2.5 补充

2.5.1 byte & 0xFF 的作用

在 Java 中,byte 类型是有符号的,取值范围为 -128 到 127。为了将 byte 转换为无符号的整数(0 到 255),可以使用 byte & 0xFF
示例:

byte b = -118;
int unsignedInt = b & 0xFF;
System.out.println(unsignedInt); // 输出 138

原理:

  • 0xFF 的二进制表示为 11111111

  • byte & 0xFF 会将 byte 的高 24 位清零,只保留低 8 位,从而得到一个无符号的整数。

2.5.2 原码、反码和补码

在计算机中,数值的表示方式有三种:原码、反码和补码。
原码
原码就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值。例如:

[+1]原 = 0000 0001
[-1]原 = 1000 0001

反码
反码的表示方法是:

  • 正数的反码是其本身。

  • 负数的反码是在其原码的基础上,符号位不变,其余各个位取反。

例如:

[+1] = [00000001]原 = [00000001]反
[-1] = [10000001]原 = [11111110]反

补码

补码的表示方法是:

  • 正数的补码就是其本身。

  • 负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后+1(即在反码的基础上+1)。

例如:

[+1] = [00000001]原 = [00000001]反 = [00000001]补
[-1] = [10000001]原 = [11111110]反 = [11111111]补

3 字符缓冲流

BufferedReaderBufferedWriter 属于字符缓冲流,它们强化了字符流 ReaderWriter

3.1 构造方法

  • BufferedReader(Reader in):创建一个新的缓冲输入流。
  • BufferedWriter(Writer out):创建一个新的缓冲输出流。

示例代码:

// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("b.txt"));

// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt"));

3.2 字符缓冲流特有方法

字符缓冲流的基本方法与普通字符流调用方式一致,这里不再赘述,我们来看字符缓冲流特有的方法。

  • BufferedReaderString readLine():读取一行数据,读取到最后返回 null
  • BufferedWriternewLine():换行,由系统定义换行符。

示例代码:

// 创建流对象
BufferedReader br = new BufferedReader(new FileReader("a.txt"));
// 定义字符串,保存读取的一行文字
String line  = null;
// 循环读取,读取到最后返回null
while ((line = br.readLine())!=null) {
    System.out.print(line);
    System.out.println("------");
}
// 释放资源
br.close();
// 创建流对象
BfferedWriter bw = new BufferedWriter(new FileWriter("b.txt"));
// 写出数据
bw.write("沉");
// 写出换行
bw.newLine();
bw.write("默");
bw.newLine();
bw.write("王");
bw.newLine();
bw.write("二");
bw.newLine();
// 释放资源
bw.close();

4 字符缓冲流练习

通过一个练习来展示字符缓冲流的使用。假设有一个文本文件,内容是乱序的《将进酒》诗句,每句前面有一个编号。我们需要将这些诗句按照编号顺序重新排列。

6.岑夫子,丹丘生,将进酒,杯莫停。
1.君不见黄河之水天上来,奔流到海不复回。
8.钟鼓馔玉不足贵,但愿长醉不愿醒。
3.人生得意须尽欢,莫使金樽空对月。
5.烹羊宰牛且为乐,会须一饮三百杯。
2.君不见高堂明镜悲白发,朝如青丝暮成雪。
7.与君歌一曲,请君为我倾耳听。
4.天生我材必有用,千金散尽还复来。

示例代码:

 /**
  * 字符缓冲流练习
  * @throws IOException
  */
 private static void testBufferReaderAndBufferWriter() throws IOException {
     // 创建map集合,保存文本数据,键为序号,值为文字
     HashMap<String, String> lineMap = new HashMap<>();
     // 创建流对象  源
     BufferedReader br = new BufferedReader(new FileReader("JavaSE/resourses/test.txt"));
     //目标
     BufferedWriter bw = new BufferedWriter(new FileWriter("JavaSE/resourses/copytest.txt"));
     // 读取数据
     String line;
     while ((line = br.readLine())!=null) {
         // 解析文本
         if (line.isEmpty()) {
             continue;
         }
         String[] split = line.split(Pattern.quote("."));
         // 保存到集合
         lineMap.put(split[0], split[1]);
     }
     // 释放资源
     br.close();
     // 遍历map集合
     for (int i = 1; i <= lineMap.size(); i++) {
         String key = String.valueOf(i);
         // 获取map中文本
         String value = lineMap.get(key);
         // 写出拼接文本
         bw.write(key+"."+value);
         // 写出换行
         bw.newLine();
     }
     // 释放资源
     bw.close();
 }

运行结果
copytest.txt的内容如下:

1.君不见黄河之水天上来,奔流到海不复回。
2.君不见高堂明镜悲白发,朝如青丝暮成雪。
3.人生得意须尽欢,莫使金樽空对月。
4.天生我材必有用,千金散尽还复来。
5.烹羊宰牛且为乐,会须一饮三百杯。
6.岑夫子,丹丘生,将进酒,杯莫停。
7.与君歌一曲,请君为我倾耳听。
8.钟鼓馔玉不足贵,但愿长醉不愿醒。

5 小结

缓冲流通过在内存中开辟缓冲区,减少了系统调用次数和磁盘读写次数,从而显著提高了 I/O 操作的效率。字节缓冲流和字符缓冲流分别适用于处理字节数据和字符数据,它们提供了高效的读写方法,使得处理大文件或频繁的 I/O 操作变得更加高效。在实际开发中,合理使用缓冲流可以大大提升程序的性能。

6 思维导图

在这里插入图片描述

7 参考链接

Java 缓冲流:Java IO 的读写效率有了质的飞升

标签:Java,缓冲,write,详解,IOException,缓冲区,new,byte
From: https://blog.csdn.net/gaosw0521/article/details/143434511

相关文章