在我的上一篇博客java -- IO流之字节流中已经介绍了java IO流中常见的字节流,字节流中大部分类都是JDK1.0出现的。在JDK1.1中对基本的IO流类库进行了重大的修改,出现了字符流Reader和Writer。
其实JDK1.1设计Reader和Writer继承层次结构主要是为了国际化。字节流仅支持8位的字节,并不能很好地处理16位的Unicode字符。由于Unicode用于字符国际化,所以添加Reader和Writer继承层次结构就是为了在所有的IO流操作中都支持Unicode。此外,由于字符流操作的是字符,所以比字节流更快。
尽管字符流操作比字节流效果更好,但是字符流终究只能操作与字符相关的,要是操作的是二进制文件或其他字符流不支持的文件,就只能使用字节流了。所以说,最明智的选择是尽量使用字符流,当字符流不能满足我们的需求时再考虑使用字节流。
Reader体系常见类
Writer体系常见类
对比java -- IO流之字节流这篇博客中的字节流体系常见类图就会发现上面字符流体系常见类图其实跟它是差不多的,不能说一致,但是基本的使用方法还是一样的。下面介绍常见字符流的基本操作。
InputStreamReader是字节流通向字符流的桥梁。在它的构造器中需要传入一个InputStream,并可以指定charset读取字节并将其解码为字符,如果没有指定charset则使用平台默认的字符集。
OutputStreamWriter是字符流通向字节流的桥梁。在它的构造器中需要传入一个OutputStream,并可以指定charset将要写入流中的字符编码成字节,如果没有指定charset则使用平台默认的字符集。
转换流图
package com.gk.io.character;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class ConversionStreamDemo {
public static void main(String[] args) throws IOException {
write("conversionStream.txt");
}
public static void write(String fileName) throws IOException {
/*
* 创建OutputStreamWriter对象,需要一个OutputStream参数
* 第二个参数为指定的字符编码,如果没有则使用平台默认字符集
*/
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(fileName), "utf-8");
osw.write("我爱java"); // 写入一个字符串
// 释放资源
osw.close();
}
}
上面演示了使用OutputStreamWriter往文件中写入数据,支持三种写入方式:写入一个字符、一个字符数组和一个字符串。这里演示的是写入一个字符串,字符和字符数组的情况请参考java -- IO流之字节流的FileInputStream & FileOutputStream一节,这里不再赘述。
存数据已经实现了,那么怎样才能取出来呢?下面演示使用InputStreamReader取文件里面的数据。
public static void read(String fileName) throws IOException {
InputStreamReader isr = new InputStreamReader(new FileInputStream(fileName), "gbk");
char[] ch = new char[1024];
isr.read(ch); // 读取一个字符数组的数据
System.out.println(new String(ch));
isr.close(); // 释放资源
System.out.println("-------------------");
char[] ch2 = new char[1024];
InputStreamReader isr2 = new InputStreamReader(new FileInputStream(fileName), "utf-8");
isr2.read(ch2);
System.out.println(new String(ch2));
isr2.close(); // 释放资源
}
上面InputStreamReader演示了使用不同的字符集解码OutputStreamWriter中使用”utf-8”写入文件的中的数据。可以看到使用"gbk"解码的时候中文出现了乱码,而使用”utf-8”却能正常的显示中文。这就说明了InputStreamReader的解码方式与OutputStreamWriter的编码方式要一致。或者像下面演示文件的复制中两者都不指定字符集,而使用平台默认的字符集。
public static void copy(String sourceFile, String destFile) throws IOException {
InputStreamReader isr = new InputStreamReader(new FileInputStream(sourceFile));
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(destFile));
int len = 0;
char[] ch = new char[1024];
while((len = isr.read(ch)) != -1){
osw.write(ch, 0, len);
}
// 释放资源
isr.close();
osw.close();
}
当我们使用本地默认编码的时候(一般程序中也都是使用本地默认编码),InputStreamReader和OutputStreamWriter就提供了相应的子类FileReader和FileWriter来简化我们的操作。这两个类都只有构造器而没有自己的方法。所有可操作的方法都是继承自父类。
package com.gk.io.character;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class FileStreamDemo {
public static void main(String[] args) throws IOException {
copy("sourceFile.txt", "destFile.txt");
}
public static void copy(String sourceFile, String destFile) throws IOException {
FileReader fr = new FileReader(sourceFile);
FileWriter fw = new FileWriter(destFile);
int len = 0;
char[] ch = new char[1024];
while((len = fr.read(ch)) != -1){
fw.write(ch, 0, len);
}
// 释放资源
fr.close();
fw.close();
}
}
当需要实现文件的追加时,可以这样使用FileWriter的构造器:FileWriter(String fileName, boolean append),append为true时表示追加。
使用FileReader或其他一些没有缓冲功能的流操作文件时,每次调用read()或readLine()都会导致从文件中读取字节,并将其转换为字符后返回,而这是极其低效的。JDK1.1为我们提供了BufferedReader,BufferedReader具有缓冲的功能,在字符输入流中读取文本的时候,能缓冲各个字符,从而实现字符、数组和行的高效读取。对应的BufferedWriter能实现单个字符、数组和字符串的高效写入。
由于大多数方法与FileReader和FileWriter差不多,这里只演示BufferedReader和BufferedWriter特有的功能。
package com.gk.io.character;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class BufferedStreamDemo {
public static void main(String[] args) throws IOException {
write("bufferedStream.txt");
}
public static void write(String bufferedStream) throws IOException {
BufferedWriter bw = new BufferedWriter(new FileWriter(bufferedStream));
bw.write("I love Java");
bw.newLine(); // 操作系统下的换行,实现了平台无关性
bw.write("I love Python,too");
// 释放资源
bw.close();
}
}
在BufferedWriter中有newLine()方法,此方法是操作系统下的换行,实现了平台无关性。如果上面代码中没有添加bw.newLine();就会出现下图结果,当然在Windows操作系统下我们可以手动添加换行符”\r\n”(bw.write("I love Java\r\n")),但是这样在别的操作下运行又不能正确的解析了,从而使程序不能实现跨平台。
public static void read(String bufferedStream) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(bufferedStream));
StringBuilder sb = new StringBuilder();
String line = br.readLine();
sb.append(line);
line = br.readLine();
sb.append(line);
System.out.println(sb.toString());
// 释放资源
br.close();
}
bufferedStream.txt文件中的"I love Java"和"I love Python,too"是有换行效果的,但是上面程序的输出却没有换行,我们看下API中对readLine()的解释:用于读取一个文本行,但是不包含任何行终止符,如果已到达流末尾,则返回null。通过解释我们就不难理解上面程序的输出结果了。
下面再用这两个方法实现对文件的高效复制。
public static void copy(String sourceFile, String destFile) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(sourceFile));
BufferedWriter bw = new BufferedWriter(new FileWriter(destFile));
String line = null;
while((line = br.readLine()) != null){
bw.write(line);
bw.newLine();
bw.flush();
}
// 释放资源
br.close();
bw.close();
}
由于readLine()方法获取的一行字符串中没有包含换行符,所以BufferedWriter写入该字符串后再用newLine()实现自动换行。flush()方法不是BufferedWriter特有的方法,Writer类就有的这个方法,所以说整个Writer体系都拥有该方法,用于刷新存放在缓冲区中的字符。我们知道使用write()往文件写入数据时,并不是一write就马上往文件写入数据,而是先存放在缓冲区中,当数据到达一定数量后(默认为8k)再一起写入文件,flush()方法可以实现手动刷新。
如果想测试BufferedReader与FileReader的效率,可参考java -- IO流之字节流中BufferedInputStream & BufferedOutputStream这一小节。
有时候我们想记录文件数据的行号,我们可以这样做:
package com.gk.io.character;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
public class LineNumberReaderDemo {
public static void main(String[] args) throws IOException {
fun();
}
public static void fun() throws IOException {
BufferedReader br = new BufferedReader(new FileReader("br.txt"));
int count = 0;
String line = null;
while((line = br.readLine()) != null){
System.out.println(++count + " : " + line);
}
// 释放资源
br.close();
}
}
定义一个计数变量count,然后累加记录行号即可。不过这样做对于喜欢偷懒的我们来说显然有些麻烦,于是java就提供了LineNumberReader给我们使用,此类是BufferedReader的直接子类,除了具有BufferedReader的缓冲作用外,最大特点就是能跟踪行号。上面代码使用LineNumberReader修改为:
public static void fun2() throws IOException {
LineNumberReader lnr = new LineNumberReader(new FileReader("br.txt"));
String line = null;
while((line = lnr.readLine()) != null){
System.out.println(lnr.getLineNumber() + " : " + line);
}
// 释放资源
lnr.close();
}
getLineNumber()方法可以获取当前行号,默认从0开始,且随数据读取在每个行结束符处递增,由于我们在while中先使用了readLine(),所以第一次获取行号getLineNumber()就递增为1了。
也可以使用setLineNumber(int)来设置起始行号,见下面例子。
public static void fun3() throws IOException {
LineNumberReader lnr = new LineNumberReader(new FileReader("br.txt"));
lnr.setLineNumber(10);
String line = null;
while((line = lnr.readLine()) != null){
System.out.println(lnr.getLineNumber() + " : " + line);
}
// 释放资源
lnr.close();
}
在使用readLine()之前先用setLineNumber(10)设置了起始行号为10,跟上面一样,由于先使用了readLine(),所以第一次getLineNumber()的时候就递增为11。
此类与下面的StringReader和字节流中的ByteArrayInputStream一样,都是用于操作内存的流,请参考java -- IO流之字节流中的ByteArrayInputStream & ByteArrayOutputStream这一小节的解释。这里只简单的演示如何使用。
package com.gk.io.character;
import java.io.CharArrayReader;
import java.io.CharArrayWriter;
import java.io.IOException;
public class CharArrayStreamDemo {
public static void main(String[] args) throws IOException {
char[] ch = write();
read(ch);
}
public static char[] write() throws IOException {
CharArrayWriter caw = new CharArrayWriter();
caw.write("I love java");
caw.close(); // 关闭此流无效
caw.write("I love python,too"); // close之后再写入不会报IOException
return caw.toCharArray();
}
public static void read(char[] ch) throws IOException {
CharArrayReader car = new CharArrayReader(ch);
int len = 0;
while((len = car.read()) != -1){
System.out.print((char)len);
}
// 释放资源
car.close();
}
}
package com.gk.io.character;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
public class StringStreamDemo {
public static void main(String[] args) throws IOException {
String str = write();
read(str);
}
public static String write() throws IOException {
StringWriter sw = new StringWriter();
sw.write("I love java");
sw.close(); // 关闭此流无效
sw.write("I love python,too"); // close之后再写入不会报IOException
return sw.toString();
}
public static void read(String str) throws IOException {
StringReader sr = new StringReader(str);
// 一次读取一个字符
/*int len = 0;
while((len = sr.read()) != -1){
System.out.print((char)len);
}*/
// 一次读取一个字符数组
char[] ch = new char[1024];
while(sr.read(ch) != -1){
System.out.println(new String(ch));
}
// 释放资源
sr.close();
}
}
PrintWriter与PrintStream类似,只不过PrintWriter支持写入字符。当启动自动刷新功能的时候,与PrintStream不同的是,PrintWriter只有在调用 println、printf 或 format 的其中一个方法时才可能完成刷新操作,而不是每当正好输出换行符时才完成。
package com.gk.io.character;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
public class PrintWriterDemo {
public static void main(String[] args) throws IOException {
write("pw.txt");
}
public static void write(String fileName) throws IOException {
PrintWriter pw = new PrintWriter(new FileWriter(fileName));
pw.println(47);
pw.println(13.14);
pw.println("I love java");
pw.println(true);
//pw.flush();
//pw.close();
}
}
上面例子没有启动自动刷新,也没有手动flush和close,因为close的时候会先刷新,所以当写入的数据小于缓冲区大小时数据是先存放在缓冲区的,也就是还没输出到文件,所以pw.txt文件是空的。下面演示开启自动刷新功能。
public static void write2(String fileName) throws IOException {
PrintWriter pw = new PrintWriter(new FileWriter(fileName),true);
pw.println(47);
pw.println(13.14);
pw.println("I love java");
pw.print(true);
//pw.flush();
//pw.close();
}
当指定构造器PrintWriter(Writer out, boolean autoFlush)中的autoFlush为true时即开启了自动刷新。前面说过当调用println、printf或 format的其中一个方法时才可能完成刷新操作,所以pw.print(true);没有输出。
自动刷新功能也给我们提供了另一种思路,前面我们在使用BufferedWriter复制文件的时候每写入一行数据都手动flush,现在我们可以使用PrintWriter改写上面的程序。
public static void copy(String sourceFile, String destFile) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(sourceFile));
PrintWriter pw = new PrintWriter(new FileWriter(destFile), true);
String line = null;
while((line = br.readLine()) != null){
pw.println(line);
}
// 释放资源
br.close();
pw.close();
}
由于println有换行功能,且在PrintWriter的构造方法中启动了自动刷新,所以上面的pw.println(line)就等价于BufferedWriter的bw.write(line);bw.newLine();bw.flush();。我们完全不用担心上面程序使用PrintWriter而出现的效率问题,在PrintWriter的内部其实帮我们封装了BufferedWriter,也就是说PrintWriter也具有BufferedWriter的高效。看下PrintWriter的源码便知。
public PrintWriter(OutputStream out, boolean autoFlush) {
this(new BufferedWriter(new OutputStreamWriter(out)), autoFlush);
// save print stream for error propagation
if (out instanceof java.io.PrintStream) {
psOut = (PrintStream) out;
}
}