在 Java 的 I/O 体系中,缓冲流(Buffered Streams)是对字节流和字符流的一种封装,通过在内存中开辟缓冲区来提高 I/O 操作的效率。Java 提供了 BufferedInputStream
和 BufferedOutputStream
来实现字节流的缓冲,以及 BufferedReader
和 BufferedWriter
来实现字符流的缓冲。本文将详细介绍缓冲流的工作原理、使用方法以及它们在实际应用中的优势。
1 缓冲流的工作原理
缓冲流的工作原理是将数据先写入缓冲区中,当缓冲区满时再一次性写入文件或输出流,或者当缓冲区为空时一次性从文件或输入流中读取一定量的数据。这样可以减少系统的 I/O 操作次数,提高系统的 I/O 效率,从而提高程序的运行效率。
2 字节缓冲流
BufferedInputStream
和 BufferedOutputStream
属于字节缓冲流,它们强化了字节流 InputStream
和 OutputStream
。
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 BufferedInputStream
的 read
方法
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 FileInputStream
的 read
方法
/**
* 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 BufferedOutputStream
的 write(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 FileOutputStream
的 write
方法
/**
* 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;
FileOutputStream
的write
方法,同样是本地方法,一次只能写入一个字节。
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 字符缓冲流
BufferedReader
和 BufferedWriter
属于字符缓冲流,它们强化了字符流 Reader
和 Writer
。
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 字符缓冲流特有方法
字符缓冲流的基本方法与普通字符流调用方式一致,这里不再赘述,我们来看字符缓冲流特有的方法。
BufferedReader
:String readLine()
:读取一行数据,读取到最后返回null
。BufferedWriter
:newLine()
:换行,由系统定义换行符。
示例代码:
// 创建流对象
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 操作变得更加高效。在实际开发中,合理使用缓冲流可以大大提升程序的性能。