当前位置: 移动技术网 > IT编程>开发语言>Java > Java开发笔记(八十六)通过缓冲区读写文件

Java开发笔记(八十六)通过缓冲区读写文件

2019年04月18日  | 移动技术网IT编程  | 我要评论

前面介绍了利用文件写入器和文件读取器来读写文件,因为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开发笔记(序)章节目录

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网