前面介绍了利用文件写入器和文件读取器来读写文件,因为FileWriter与FileReader读写的数据以字符为单位,所以这种读写文件的方式被称作“字符流I/O”,其中字母I代表输入Input,字母O代表输出Output。可是FileWriter的读操作并不高效,缘由在于FileWriter每次调用write方法都会直接写入文件,假如某项业务需要多次调用write方法,那么程序就会写入文件同样次数。因为写文件本质是写磁盘,磁盘的速度远不如内存,所以频繁地写文件必然严重降低程序的运行效率。为此Java又设计了缓存写入器BufferedWriter,它的write方法并不直接写入文件,而是先写入一块缓存,等到缓存写满了再将缓存上的数据写入文件。由于缓存空间位于内存之中,写入缓存等同访问内存,这样相当于把写磁盘动作替换成写内存动作,因此BufferedWriter的整体写文件性能要大大优于FileWriter。除此之外,BufferedWriter还新增了下列几个方法:
newLine:往文件末尾添加换行标记(Window系统是回车加换行)。当然实际上是先往缓存添加换行标记,并非直接往磁盘写入换行标记。
flush:立即将缓冲区中的数据写入磁盘。默认情况要等缓冲区满了才会写入磁盘,或者调用close方法关闭文件之时也会写入磁盘,但是有时程序猴急,一定要立即写入磁盘,此时就需调用flush方法强行写磁盘。
使用缓存写入器之前要先创建文件读取器对象,并获得父类Writer的实例,然后再据此创建缓存写入器对象。下面是通过缓存写入器把多行字符串写入文件的代码例子:

	private static String mSrcName = "D:/test/aad.txt";
// 使用缓存字符流写入文件
private static void writeBuffer() {
String str1 = "白日依山尽,黄河入海流。";
String str2 = "欲穷千里目,更上一层楼。";
File file = new File(mSrcName); // 创建一个指定路径的文件对象
// try(...)允许在圆括号内部拥有多个资源创建语句,语句之间以冒号分隔
// 先创建文件写入器,再根据文件读取器创建缓存写入器
try (Writer writer = new FileWriter(file);
BufferedWriter bwriter = new BufferedWriter(writer);) {
// FileWriter的每次write调用都会直接写入磁盘,不但效率低,性能也差。
// BufferedWriter的每次write调用会先写入缓冲区,直到缓冲区满了才写入磁盘,
// 缓冲区大小默认是8K,查看源码defaultCharBufferSize = 8192;
// 资源释放的close方法再把缓冲区的剩余数据写入磁盘,
// 或者中途调用flush方法也可提前将缓冲区的数据写入磁盘。
bwriter.write(str1); // 往文件写入字符串
bwriter.newLine(); // 另起一行,也就是在文件末尾添加换行标记(Window系统是回车加换行)
bwriter.write(str2); // 往文件写入字符串
//bwriter.flush(); // 把缓冲区中的数据写入磁盘
} catch (Exception e) {
e.printStackTrace();
}
}

既然文件写入器有对应的缓存写入器,那么文件读取器也有对应的缓存读取器BufferedReader。BufferedReader的实现原理与它的兄弟BufferedWriter类似,另外BufferedReader比起文件读取器新增了如下方法:
readLine:从文件中读取一行数据。
mark:在当前位置做个标记。
reset:重置文件指针,令其回到上次标记的位置。也就是回到上次mark方法标记的文件位置。
lines:读取文件内容的所有行,返回的是Stream<String>流对象,之后便可按照流式处理来加工该字符串流。
若想使用缓存读取器,依然要先创建文件读取器,再根据其父类的读取器实例创建缓存读取器。下面是通过缓存读取器从文件中读取多行字符串的代码例子:

	// 使用缓存字符流读取文件
private static void readBuffer() {
File file = new File(mSrcName); // 创建一个指定路径的文件对象
// try(...)允许在圆括号内部拥有多个资源创建语句,语句之间以冒号分隔
// 先创建文件读取器,再根据文件读取器创建缓存读取器
try (Reader reader = new FileReader(file);
BufferedReader breader = new BufferedReader(reader);) {
breader.mark((int) file.length()); // 做个标记
for (int i=1; ; i++) {
// FileReader只能一个字符一个字符地读,或者一次性读进字符数组。
// BufferedReader还支持一行一行地读。
String line = breader.readLine(); // 从文件中读出一行文字
if (line == null) { // 读到了空指针,表示已经到了文件末尾
break;
}
System.out.println("第"+i+"行的文字为:"+line);
}
breader.reset(); // 重置文件指针,令其回到上次标记的位置
for (int i=1; ; i++) {
String line = breader.readLine(); // 从文件中读出一行文字
if (line == null) { // 读到了空指针,表示已经到了文件末尾
break;
}
System.out.println("又读了一遍 第"+i+"行的文字为:"+line);
}
//breader.lines(); // 返回Stream<String>对象,之后可按照流式处理来加工该字符串流
} catch (Exception e) {
e.printStackTrace();
}
}

注意到以上代码BufferedWriter和BufferedReader的创建语句都位于try后面的圆括号之中,这是因为Writer与Reader两大家族统统实现了AutoCloseable接口,所以由它们繁衍而来的所有子类都具备自动释放资源的功能。另外,try语句支持同时管理多个资源类,只要它们的对象创建语句以冒号隔开,程序在运行时即可自动回收相关的资源。
结合运用读操作和写操作,可以实现文件复制的功能,无非是一边从源文件中读出数据,另一边紧接着往目标文件写入数据。采用缓存读取器和缓存写入器逐行复制的话,具体的文件复制代码示例如下:

	private static String mSrcName = "D:/test/aad.txt";
private static String mDestName = "D:/test/aad_copy.txt";
// 通过缓存字符流逐行复制文件
private static void copyFile() {
File src = new File(mSrcName); // 创建一个指定路径的源文件对象
File dest = new File(mDestName); // 创建一个指定路径的目标文件对象
// try(...)允许在圆括号内部拥有多个资源创建语句,语句之间以冒号分隔
// 分别创建源文件的缓存读取器,以及目标文件的缓存写入器
try (BufferedReader breader = new BufferedReader(new FileReader(src));
BufferedWriter bwriter = new BufferedWriter(new FileWriter(dest));) {
for (int i=0; ; i++) {
String line = breader.readLine(); // 从文件中读出一行文字
if (line == null) { // 读到了空指针,表示已经到了文件末尾
break;
}
if (i != 0) { // 第一行开头不用换行
bwriter.newLine(); // 另起一行,也就是在文件末尾添加换行标记
}
bwriter.write(line); // 往文件写入字符串
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("文件复制完成,源文件大小="+src.length()+",新文件大小="+dest.length());
}

或者也可逐个字符来复制文件,此时BufferedReader每次调用的read方法只返回整型数表示一个字符,并且BufferedWriter每次调用的write方法也只写入该字符对应的整型数。通过依次遍历源文件的所有字符,同时往目标文件依次写入这些字符,从而完成逐个字符复制文件的操作流程。下面是采取逐字符复制文件的代码例子:

	// 通过缓存字符流逐个字符复制文件
private static void copyFileByInt() {
File src = new File(mSrcName); // 创建一个指定路径的源文件对象
File dest = new File(mDestName); // 创建一个指定路径的目标文件对象
// try(...)允许在圆括号内部拥有多个资源创建语句,语句之间以冒号分隔
// 分别创建源文件的缓存读取器,以及目标文件的缓存写入器
try (BufferedReader breader = new BufferedReader(new FileReader(src));
BufferedWriter bwriter = new BufferedWriter(new FileWriter(dest));) {
while (true) { // 开始遍历文件中的所有字符
int temp = breader.read(); // 从源文件中读出一个字符
if (temp == -1) { // read方法返回-1表示已经读到了文件末尾
break;
}
bwriter.write(temp); // 往目标文件写入一个字符
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("文件复制完成,源文件大小="+src.length()+",新文件大小="+dest.length());
}

需要注意的是,使用字符流复制文件只有逐行复制和逐字符复制两种方式,不可采取整个读到字符数组再整个写入字符数组的方式。之所以不能通过字符数组复制文件,是因为中文跟英文不一样,一个汉字会占用多个字节(GBK编码的每个汉字占用两个字节,UTF8编码的每个汉字占用三个字节)。若要把文件内容读到字符数组,势必先得知晓该数组的长度,可是调用文件对象的length方法只能得到该文件的字节长度,并非字符长度。譬如“白日依山尽”这个字符串在内存中的字符数组长度为5,写到UTF8编码的文件之后,文件大小是5*3=15字节;接着想把文件内容读到字符数组,然而15字节的文件天晓得它有几个字符,可能有5个UTF8编码的中文字符,也可能有15个英文字符,也可能有5个GBK编码的中文字符加5个英文字符共10个字符,总之你根本想不到该分配多大的字符数组。既然确定不了待读取的字符数组长度,就无法一字不差地复制文件内容了。

更多Java技术文章参见《Java开发笔记(序)章节目录

Java开发笔记(八十六)通过缓冲区读写文件的更多相关文章

  1. Java开发笔记(十六)非此即彼的条件分支

    前面花了大量篇幅介绍布尔类型及相应的关系运算和逻辑运算,那可不仅仅是为了求真值或假值,更是为了通过布尔值控制流程的走向.在现实生活中,常常需要在岔路口抉择走去何方,往南还是往北,向东还是向西?在Jav ...

  2. Java开发笔记(九十六)线程的基本用法

    每启动一个程序,操作系统的内存中通常会驻留该程序的一个进程,进程包含了程序的完整代码逻辑.一旦程序退出,进程也就随之结束:反之,一旦强行结束进程,程序也会跟着退出.普通的程序代码是从上往下执行的,遇到 ...

  3. Java开发学习(三十六)----SpringBoot三种配置文件解析

    一. 配置文件格式 我们现在启动服务器默认的端口号是 8080,访问路径可以书写为 http://localhost:8080/books/1 在线上环境我们还是希望将端口号改为 80,这样在访问的时 ...

  4. Java开发学习(二十六)----SpringMVC返回响应结果

    SpringMVC接收到请求和数据后,进行了一些处理,当然这个处理可以是转发给Service,Service层再调用Dao层完成的,不管怎样,处理完以后,都需要将结果告知给用户. 比如:根据用户ID查 ...

  5. Java开发笔记(十八)上下求索的while循环

    循环是流程控制的又一重要结构,“白天-黑夜-白天-黑夜”属于时间上的循环,古人“年复一年.日复一日”的“日出而作.日落而息”便是每天周而复始的生活.计算机程序处理循环结构时,给定一段每次都要执行的代码 ...

  6. Java学习笔记(十六)——Java RMI

    [前面的话] 最近过的好舒服,每天过的感觉很充实,一些生活和工作的技巧注意了就会发现,其实生活也是可以过的如此的有滋有味,满足现在的状况,并且感觉很幸福. 学习java RMI的原因是最近在使用dub ...

  7. Java开发笔记(十九)规律变化的for循环

    前面介绍while循环时,有个名叫year的整型变量频繁出现,并且它是控制循环进出的关键要素.不管哪一种while写法,都存在三处与year有关的操作,分别是“year = 0”.“year<l ...

  8. .net开发笔记(十六) 对前部分文章的一些补充和总结

    补充有两个: 一个是系列(五)中讲到的事件编程(网址链接),该文提及到了事件编程的几种方式以及容易引起的一些异常,本文补充“多线程事件编程”这一块. 第二个是前三篇博客中提及到的“泵”结构在编程中的应 ...

  9. 【Java学习笔记之十六】浅谈Java中的继承与多态

    1.  什么是继承,继承的特点? 子类继承父类的特征和行为,使得子类具有父类的各种属性和方法.或子类从父类继承方法,使得子类具有父类相同的行为. 特点:在继承关系中,父类更通用.子类更具体.父类具有更 ...

  10. Java开发笔记(十)一元运算符的技巧

    前面讲到赋值运算符的时候,提到“x = x+7”可以被“x += 7”所取代,当然Java编程中给某个变量自加7并不常见,常见的是给某变量自加1,就像走台阶,一般都是一级一级台阶地走,犯不着一下子跳上 ...

随机推荐

  1. python之算法排序模块

    这是一个能够随时学习重要算法的Python模块,记录在案,方便查看 特点 易于使用 容易理解的文档 快速获取算法的源代码 随时获取时间复杂度 安装 仅需在终端中执行以下命令: pip3 install ...

  2. Java开发者必备的10大学习网站,送给入门学习java的你,请收下!

    作为开发者来说,必备的除了对编码的热情还要有自己的一套技巧,另外不可缺少的就是平时学习的网站.以下本人收集的 Java 开发者必备的网站,这些网站可以提供信息.以及一些很棒的讲座 , 还能解答一般问题 ...

  3. Java实现大批量数据导入导出(100W以上) -(一)导入

    最近业务方有一个需求,需要一次导入超过100万数据到系统数据库.可能大家首先会想,这么大的数据,干嘛通过程序去实现导入,为什么不直接通过SQL导入到数据库. 大数据量报表导出请参考:Java实现大批量 ...

  4. python中线程和进程(一)

    目录 进程和线程 Python中的线程 1. Thread类 2. 线程的启动 3. 线程的传参 4. 线程的属性和方法 5. daemon线程和non-daemon线程 6. join方法 7. 定 ...

  5. RabbitMQ 消息队列 入门 第二章(交换类型fanout)

    1.安装完 RabbitMQ 之后,我们可以点击  http://localhost:15672/#/  默认账号:guest  密码: guest  在这上面我们可以查看执行情况.管理连接.管理队列 ...

  6. Unity C#笔记 协程

    什么是协程 协同程序,在主程序运行的同时,开启另外一段逻辑处理,来协同当前程序的执行. 可能看了这段文字介绍还是有点模糊,其实可以用多线程来比较. 多线程 多线程,顾名思义,多条同时执行的线程. 最初 ...

  7. Asp.NetCore轻松学-使用Docker进行容器化托管

    前言 没有 docker 部署的程序是不完整的,在写了 IIS/Centos/Supervisor 3篇托管介绍文章后,终于来到了容器化部署,博客园里面有关于 docker 部署的文章比比皆是,作为硬 ...

  8. 同一容器中a标签比较多的情况下通过title属性值隐藏

    同一容器中a标签比较多的情况下如何通过title属性值控制a标签的隐藏或显示 最近项目中遇到一个IE兼容性问题,网站需要在底部footer添加"站长统计"代码,容器中动态添加很多a ...

  9. Storm入门(十四)Trident API Overview

    The core data model in Trident is the "Stream", processed as a series of batches. A stream ...

  10. vue组件-构成组件-父子组件相互传递数据

    组件对于vue来说非常重要,学习学习了基础vue后,再回过头来把组件弄透! 一.概念 组件意味着协同工作,通常父子组件会是这样的关系:组件 A 在它的模版中使用了组件 B . 它们之间必然需要相互通信 ...