之前一篇文章讲述了Java中字节流的分类及简单用法,最后我们可以发现,Java中的字节流既可以用来处理二进制文件,也可以用来处理文本文件。但是字节流对于文本文件的处理是不太方便的,比如字节流的媒介是字节,但是文本文件的内容都是可显式地字符,很明显通过字符作为媒介更合理(需要手动进行字节和字符通过某种特定编码格式的转化)。另外,在之前那篇文章Java编程拾遗『搞定编码』中,我们了解到,字符可以有很多种编码方式,但是在使用字节流处理文本文件,编码这一块就不是很方便处理,因为字节流是没有编码地概念的。但是文本或者讲字符串,是Java中比较常见地一种形式,能方便地进行处理还是很重要的,所以字符流就出现了。可以讲,字符流就是用来方便处理可显示字符的。字符流的继承体系如下:
![](http://cdn.lidol.top/lidol_blog/java_reader_system.jpg)
本文重点介绍如下几种字符流:
- Reader/Writer:抽象类,也是字符流的基类
- InputStreamReader/OutputStreamWriter:适配器类,将字节流转换为字符流
- FileReader/FileWriter:输入源和输出目标是文件的字符流
- CharArrayReader/CharArrayWriter: 输入源和输出目标是char数组的字符流
- StringReader/StringWriter:输入源和输出目标是String的字符流
- BufferedReader/BufferedWriter:装饰类,对输入输出流提供缓冲,以及按行读写功能
- PrintWriter:装饰类,可将基本类型和对象转换为其字符串形式输出的类
1. Reader/Writer
Reader和Writer是抽象类,是所有字符流的基类。
1.1 Reader
Reader中的方法跟InputStream中的方法类似,区别在于InputStream读取的单位是字节,Reader读取的单位char。
S.N. | 方法 | 说明 |
1 | public int read() throws IOException | 从字符输入流Reader中读取一个字符,返回值为读取字符对应的int值,返回-1表示字符流中的内容已经读取结束 |
2 | public int read(char cbuf[]) throws IOException | 从字符输入流Reader中读取多个字节,放入字符数组cbuf中,返回值为实际读入的字符个数 |
3 | abstract public int read(char cbuf[], int off, int len) throws IOException | 从字符输入流Reader中读取len个字节放入字符数组cbuf index off开始的位置, 返回值为实际读入的字符个数 |
4 | public boolean ready() throws IOException | 返回字符输入流Reader下一次read()操作是否需要阻塞,如果需要阻塞返回true,否则返回false。跟InputStream中的available方法类似。 |
5 | public long skip(long n) throws IOException | 跳过字符输入流Reader n个字符,返回值为实际跳过的字符个数 |
6 | public boolean markSupported() | 判断当前字符输入流是否支持mark/reset操作 |
7 | public void mark(int readAheadLimit) throws IOException | 标记能够从字符输入流中往后读取的字符个数readAheadLimit |
8 | public void reset() throws IOException | 重新从标记位置读取字符输入流 |
9 | abstract public void close() throws IOException | 关闭字符输入流,释放资源 |
方法大体上跟InputStream很类似,不同的是,InputStream读取的单位是字节,Reader读取的单位是char。
1.1 Writer
S.N. | 方法 | 说明 |
1 | public void write(int c) throws IOException | 向字符输出流中写入一个字符 |
2 | public void write(char cbuf[]) throws IOException | 将字符数组cbuf中所有的字符写入字符输出流 |
3 | abstract public void write(char cbuf[], int off, int len) throws IOException | 将字符数组cbuf中从index off开始,长度为len的字符写入字符输出流 |
4 | public void write(String str) throws IOException | 将字符串str中所有的字符写入到字符输出流 |
5 | public void write(String str, int off, int len) throws IOException | 将字符串str中index 从off开始长度为len的所有字符写入到字符输出流 |
6 | public Writer append(char c) throws IOException | 操作等同于write(int c),向字符输出流中写入一个字符 |
7 | public Writer append(CharSequence csq) throws IOException | 操作等同于write(String str),将CharSequence中所有的字符写入到字符输出流 |
8 | public Writer append(CharSequence csq, int start, int end) throws IOException | 将CharSequence中index再[start, end)区间内所有的字符写入到字符输出流 |
9 | abstract public void flush() throws IOException | 将缓冲而未实际写的数据进行实际写入 |
10 | abstract public void close() throws IOException | 关字符输出流,释放资源 |
2. InputStreamReader/OutputStreamWriter
InputStreamReader和OutputStreamWriter是适配器类,负责InputStream/OutputStream和Reader/Writer之间的转换。
2.1 OutputStreamWriter
OutputStreamWriter可以将字节输出流OutputStream转换writer。举个例子,如果要将字符串写入一个文本文件中,直接通过FileOutputStream是很不方便的(需要先将字符串通过某种编码转化为字节数组,然后将字节数组通过FileOutputStream最终写入到文件中),但是通过OutputStreamWriter将FileOutputStream转化为Writer后,就可以很方便地直接将字符串写入文件。OutputStreamWriter可以通过如下方式构建:
public OutputStreamWriter(OutputStream out)
public OutputStreamWriter(OutputStream out, Charset cs)
public OutputStreamWriter(OutputStream out, String charsetName)
参数out为待适配的字节输出流OutputStream对象,第二个参数cs/charSetName为用来将字符编码的字符集,在将字符转化为字节数组时使用。如果没有传,则使用系统默认编码。
2.2 InputStreamReader
InputStreamReader可以将字节输入流InputStream转化为Reader,可以方便地通过字符/字符串读取字节输入流InputStream中的内容。可以通过如下方式构建:
public InputStreamReader(InputStream in)
public InputStreamReader(InputStream in, Charset cs)
public InputStreamReader(InputStream in, String charsetName)
跟OutPutStreamWriter构造函数类似,参数in为待适配的InputStream对象,第二个参数cs/charSetName为用来将字符编码的字符集,在将字节转化为字符时使用。 如果没有传,则使用系统默认编码。
2.3 示例
@SneakyThrows
public static void main(String[] args) {
try (OutputStream fileOutputStream = new FileOutputStream("d:/hello.txt");
Writer outPutStreamWriter = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8)) {
String text = "hello 卓立";
outPutStreamWriter.write(text);
}
try (InputStream fileInputStream = new FileInputStream("d:/hello.txt");
Reader inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8)) {
char[] cbuf = new char[1024];
int charsRead = inputStreamReader.read(cbuf);
System.out.println(new String(cbuf, 0, charsRead));
}
}
运行结果:
hello 卓立
可以看到,通过OutputStreamWriter,将FileOutputStream对象转化为Writer实例,然后直接调用Writer的write方法,直接将字符串通过UTF-8编码写入到文件中。然后通过InputStreamReader,将FileInputStream对象转化为Reader实例,然后直接调用Reader的read方法,直接将文件中的二进制内容通过UTF-8编码,读取到字符数组cbuf中,并转化为String打印输出。
3. FileReader/FileWriter
FileReader/FileWriter是输入源和输出目标是文件的字符流,FileReader继承了InputStreamReader,FileWriter继承了OutputStreamWriter。
2.1 FileWriter
FileWriter可以方便地将字符/字符串写入文件,从这个角度上讲,FileWriter和OutputStreamWriter非常类似。但是不同的是,FileWriter不能指定编码类型,只能使用系统默认编码,而OutputStreamWriter可以指定编码。可以通过如下方式构造FileWriter:
public FileWriter(File file) throws IOException
public FileWriter(File file, boolean append) throws IOException
public FileWriter(String fileName) throws IOException
public FileWriter(String fileName, boolean append) throws IOException
参数file/fileName,表示要输出的文件。第二个参数append,表示是追加文件还是覆盖文件。除了构造函数外,其他方法都试继承自OutputStreamWriter类。
2.2 FileReader
FileReader可以方便地将文件内容读取为字符/字符串,跟InputSreamReader的功能非常类似。同时,FileReader也不能指定编码类型,只能使用系统默认编码,而InputStreamReader可以指定编码。可以通过如下方式构造FileReader:
public FileReader(File file) throws FileNotFoundException
public FileReader(String fileName) throws FileNotFoundException
跟FileWriter类似,FileReader类中也只定义了构造函数,其他方法都继承自InputStreamReader。
2.3 示例
@SneakyThrows
public static void main(String[] args) {
try (Writer fileWriter = new FileWriter("d:/hello.txt")) {
String text = "hello 卓立";
fileWriter.write(text);
}
try (Reader fileReader = new FileReader("d:/hello.txt")) {
char[] cbuf = new char[1024];
int charsRead = fileReader.read(cbuf);
System.out.println(new String(cbuf, 0, charsRead));
}
}
运行结果:
hello 卓立
4. CharArrayReader/CharArrayWriter
CharArrayReader/CharArrayWriter是源和目的地是char数组的字符流,跟字节流中的ByteArrayInputStream/ByteArrayOutputStream的功能类似。
4.1 CharArrayWriter
任何一个Reader的实现类都可以实现将输入流中的内容读取为字符数组,但是有这样一个问题Reader是无法解决的,那就是用来存储输入流中内容的字符数组大小需要预先定义,如果不够大,也不方便扩展。这时候就可以借助CharArrayWriter类,先通过Reader将输入流中的内容读取到一个固定大小的字符数组中,如果超过固定大小的字符数组,可分多次读取,然后将每次读取的内容依次写入到CharArrayWriter对象中,最后调用CharArrayWriter的toCharArray方法获取一个完整的数组。CharArrayWriter可以通过如下方式构建:
public CharArrayWriter()
public CharArrayWriter(int initialSize)
initialSize表示CharArrayWriter内部用来存储写入内容的char数组的大小,第一个构造函数初始化的char数组默认大小为32,如果写入时char数组容量不够,会进行动态扩容,每次扩容为之前的两倍。除了构造方法之外,CharArrayWriter对Writer接口的方法进行了重写。其余方法参考上面Writer类中的方法说明,这里不再讲了。
4.2 CharArrayReader
CharArrayReader与上篇文章讲的的ByteArrayInputStream类似,它可以将char数组包装为Reader,是一种适配器模式,可以通过如下方式构建:
public CharArrayReader(char buf[])
public CharArrayReader(char buf[], int offset, int length)
第一个构造函数是将char数组buf中的全部内容包装到CharArrayReader(效果是buf数组中的全部char都体现在CharArrayReader中)。第二个构造函数是将char数组buf中index从offset开始长度为length的所有char包装到CharArrayReader(效果是buf数组中offset到offset + len -1的char体现在CharArrayReader中,read操作也只能读取到offset及之后的char)。CharArrayReader的所有数据都在内存,支持mark/reset重复读取。
4.3 示例
@SneakyThrows
public static void main(String[] args) {
try (Reader inputStreamReader = new InputStreamReader(new FileInputStream("d:/hello.txt"), StandardCharsets.UTF_8)) {
CharArrayWriter charArrayWriter = new CharArrayWriter();
char[] cbuf = new char[1024];
int charsRead;
//分多次将Reader地内容读出,并写到writer中
while ((charsRead = inputStreamReader.read(cbuf)) != -1) {
charArrayWriter.write(cbuf, 0, charsRead);
}
System.out.println(charArrayWriter.toString());
}
char[] chars = "卓立 test".toCharArray();
//调用CharArrayReader()构造函数初始化CharArrayReader
try (CharArrayReader charArrayReader = new CharArrayReader(chars);
CharArrayWriter charArrayWriter = new CharArrayWriter()) {
writeChars(charArrayReader, charArrayWriter);
}
//调用CharArrayReader(char buf[], int offset, int length)构造函数初始化CharArrayReader
try (CharArrayReader charArrayReader = new CharArrayReader(chars, 1, 3);
CharArrayWriter charArrayWriter = new CharArrayWriter()) {
writeChars(charArrayReader, charArrayWriter);
}
}
@SneakyThrows
private static void writeChars(CharArrayReader charArrayReader, CharArrayWriter charArrayWriter) {
char[] buf = new char[16];
int charsRead;
while ((charsRead = charArrayReader.read(buf)) != -1) {
charArrayWriter.write(buf, 0, charsRead);
}
String data = charArrayWriter.toString();
System.out.println(data);
}
运行结果:
hello 卓立
卓立 test
立 t
5. StringReader/StringWriter
StringReader/StringWriter是输入源和输出目标是String的字符流,这样描述其实也不完全正确,因为StringWriter类中实际是使用StringBuffer存储write方法写入的内容的。
5.1 StringWriter
将数据通过StringWriter的write操作写入,其实就是将数据写入到StringWriter内部的StringBuffer中了。实现了数据的“写出”,只不过跟CharArrayWriter类似,写出的数据还在内存中。StringWriter可以通过如下方式构造:
public StringWriter()
public StringWriter(int initialSize)
第二个构造函数的参数initialSize,表示StringWriter中用来存储写入内容的StringBuffer初始化时的容量。第一个构造函数会调用StringBuffer的无参构造函数初始化一个StringBuffer实例对象。通过之前讲的StringBuffer的内容可知,StringBuffer的无参构造函数,其其实内部char数组声明的大小为16,而如果指定大小,其内部char数组的大小即为指定的大小。StringWriter内部使用了StringBuffer存储写入的内容,最终其实也等价于使用char数组,并且也可以动态扩容,本质上跟CharArrayWriter没什么区别。
5.2 StringReader
StringReader跟CharArrayReader一样,也是一种适配器模式,可以将字符串包装为字符流。可以通过如下方式构建:
public StringReader(String s)
5.3 示例
@SneakyThrows
public static void main(String[] args) {
try (Reader inputStreamReader = new InputStreamReader(new FileInputStream("d:/hello.txt"), StandardCharsets.UTF_8)) {
StringWriter stringWriter = new StringWriter();
char[] cbuf = new char[1024];
int charsRead;
//分多次将Reader地内容读出,并写到writer中
while ((charsRead = inputStreamReader.read(cbuf)) != -1) {
stringWriter.write(cbuf, 0, charsRead);
}
System.out.println(stringWriter.toString());
}
String str = "卓立 test";
try (StringReader stringReader = new StringReader(str);
StringWriter stringWriter = new StringWriter()) {
writeString(stringReader, stringWriter);
}
}
@SneakyThrows
private static void writeString(StringReader stringReader, StringWriter stringWriter) {
char[] buf = new char[16];
int charsRead;
while ((charsRead = stringReader.read(buf)) != -1) {
stringWriter.write(buf, 0, charsRead);
}
String data = stringWriter.toString();
System.out.println(data);
}
运行结果:
hello 卓立
卓立 test
6. BufferedReader/BufferedWriter
BufferedReader/BufferedWriter直接继承自Reader/Writer,是装饰类,可为普通普通字符流提供缓冲及按行读写功能
6.1 BufferedWriter
BufferedWriter提供基础字符输出流缓冲写和按行写的能力,是一种装饰器模式。可以通过如下方式构建:
public BufferedWriter(Writer out)
public BufferedWriter(Writer out, int sz)
out表示待包装的基础字符输出流,sz表示缓冲大小,第一个构造函数中,没有指定缓冲大小,默认为8192。BufferedWriter继承自Writer,对于继承自Writer类的方法不再单独讲述了,参考上面的Writer方法列表,这里列一下BufferedWriter中新增的按行写的方法:
public void newLine() throws IOException
该方法可以输出当前环境下特定的换行符
6.2 BufferedReader
BufferedReader提供基础字符输入流缓冲读和按行读的能力,是一种装饰器模式。可以通过如下方式构建:
public BufferedReader(Reader in)
public BufferedReader(Reader in, int sz)
in表示待包装的字符输入流,sz表示缓冲的大小,第一个构造函数中,没有指定缓冲大小,默认为8192。BufferedReader继承自Reader,对于继承自Reader类的方法不再单独讲述了,参考上面的Reader方法列表,这里列一下BufferedReader中新增的按行读取的方法:
public String readLine() throws IOException
该方法返回一行内容,当读到流结尾时,返回null
6.3 示例
FileReader/FileWriter读写时是没有缓冲的,也不能按行读写,这里通过装饰类BufferedReader/BufferedWriter提供缓冲和按行读写功能:
@SneakyThrows
public static void main(String[] args) {
try (FileWriter fileWriter = new FileWriter("d:/hello.txt");
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter)) {
bufferedWriter.write("first line");
bufferedWriter.newLine();
bufferedWriter.write("second line");
bufferedWriter.newLine();
bufferedWriter.write("third line");
}
try (FileReader fileReader = new FileReader("d:/hello.txt");
BufferedReader bufferedReader = new BufferedReader(fileReader)) {
String line;
while ((line = bufferedReader.readLine()) != null ) {
System.out.println(line);
}
}
}
运行结果:
first line
second line
third line
7. PrintWriter
PrinterWriter直接继承了Writer类,它有很多构造函数,可以接受文件路径名、文件对象、OutputStream、Writer等。同时提供了很多自定义方法,可以方便的将各种基本类型数据、对象转化为字符串输出,也可以实现按行输出,格式化输出。在输出到文件时,可以优先选择该类。PrinterWriter可以通过如下方式构建:
S.N. | 方法 | 说明 |
1 | public PrintWriter (Writer out) | 装饰模式,将普通字符输出流包装为PrintWriter |
2 | public PrintWriter(Writer out, boolean autoFlush) | 装饰模式,将普通字符输出流包装为PrintWriter,autoFlush表示同步缓冲区的时机,如果为true,则在调用print、printf、format方法时会同步缓冲区,否则需要根据情况调用flush方法 |
3 | public PrintWriter(OutputStream out) | 适配器模式,将字节输出流转化为PrintWriter |
4 | public PrintWriter(OutputStream out, boolean autoFlush) | 适配器模式,将字节输出流转化为PrintWriter,autoFlush参数含义同上 |
5 | public PrintWriter(String fileName) throws FileNotFoundException | 构造输出目标时文件的PrintWriter,参数fileName表示文件路径 |
6 | public PrintWriter(String fileName, String csn) | 构造输出目标时文件的PrintWriter,参数fileName表示文件路径,参数csn表示字符集,用于指定输出到文件时的编码,上面没指定字符集时,表示使用系统默认字符集 |
7 | public PrintWriter(File file) throws FileNotFoundException | 构造输出目标时文件的PrintWriter,参数file为文件对象 |
8 | public PrintWriter(File file, String csn) | 构造输出目标时文件的PrintWriter,参数file表示文件对象,参数csn表示字符集,用于指定输出到文件时的编码,上面没指定字符集时,表示使用系统默认字符集 |
public PrintWriter (Writer out) {
this(out, false);
}
public PrintWriter(Writer out, boolean autoFlush) {
super(out);
this.out = out;
this.autoFlush = autoFlush;
lineSeparator = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction("line.separator"));
}
public PrintWriter(OutputStream out) {
this(out, false);
}
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;
}
}
public PrintWriter(String fileName) throws FileNotFoundException {
this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName))),
false);
}
public PrintWriter(String fileName, String csn) throws FileNotFoundException, UnsupportedEncodingException {
this(toCharset(csn), new File(fileName));
}
public PrintWriter(File file) throws FileNotFoundException {
this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))), false);
}
public PrintWriter(File file, String csn) throws FileNotFoundException, UnsupportedEncodingException {
this(toCharset(csn), file);
}
private PrintWriter(Charset charset, File file) throws FileNotFoundException {
this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), charset)), false);
}
- 对于参数类型时文件路径、文件对象和OutputStream的构造函数,PrintWriter内部会构造一个BufferedWriter用于缓冲。但是对于Writer为参数的构造函数,PrinterWriter内部就不包装BufferedWriter了。
- 参数csn表示字符集,可以控制写入文件时的编码格式,是通过构造OutputStreamWriter时生效的,具体可以参见上述最后一个私有的构造函数。
- 构造方法中的参数autoFlush表示同步缓冲区的时机,如果为true,则在调用println、printf或format方法的时候,同步缓冲区,如果没有传,则不会自动同步,需要根据情况调用flush方法。
下面来看一下PrintWriter类中除了继承字Writer类之外的其它方法:
S.N. | 方法 | 说明 |
1 | public PrintWriter append(char c) | 向writer中追加一个char |
2 | public PrintWriter append(CharSequence csq) | 向writer中追加一个CharSequence |
3 | public PrintWriter append(CharSequence csq, int start, int end) | 将CharSequence c从index start到index end的子串追加到writer中 |
4 | public PrintWriter format(Locale l, String format, Object … args) | 使用指定的格式字符串和参数将格式化的字符串写入此writer。 如果启用了自动刷新,则调用此方法将刷新输出缓冲区。l表示格式化过程中使用的语言环境 |
5 | public PrintWriter format(String format, Object … args) | 同上format方法,只是没指定语言环境,而使用默认语言环境 |
6 | public void print(boolean b) | 向writer中写入一个boolean基本类型变量,如果为true,写入字符串”true”,否则写入”false” |
7 | public void print(char c) | 向writer中写入一个char基本类型变量 |
8 | public void print(char s[]) | 向writer中写入一个char数组 |
9 | public void print(double d) | 向writer中写入一个double基本类型变量,写入的实际是调用String.valueOf(d)返回的字符串 |
10 | public void print(float f) | 向writer中写入一个float基本类型变量,写入的实际是调用String.valueOf(f)返回的字符串 |
11 | public void print(int i) | 向writer中写入一个int基本类型变量,写入的实际是调用String.valueOf(i)返回的字符串 |
12 | public void print(long l) | 向writer中写入一个long基本类型变量,写入的实际是调用String.valueOf(l)返回的字符串 |
13 | public void print(Object obj) | 向writer中写入一个Object对象,写入的实际是调用String.valueOf(obj)返回的字符串 |
14 | public void print(String s) | 向writer中写入一个String对象,如果s为null,写入”null” |
15 | public PrintWriter printf(Locale l, String format, Object … args) | 使用指定的格式字符串和参数将格式化的字符串写入此writer,同4 |
16 | public PrintWriter printf(String format, Object … args) | 使用指定的格式字符串和参数将格式化的字符串写入此writer,同5 |
17 | public void println() | 通过向Writer写入行分隔符字符串来实现换行 |
18 | public void println(boolean x) | 向writer中写入一个boolean基本类型变量并换行 |
19 | public void println(char x) | 向writer中写入一个char基本类型变量并换行 |
20 | public void println(char x[]) | 向writer中写入一个char数组并换行 |
21 | public void println(double x) | 向writer中写入一个double基本类型变量并换行 |
22 | public void println(float x) | 向writer中写入一个float基本类型变量并换行 |
23 | public void println(int x) | 向writer中写入一个int基本类型变量并换行 |
24 | public void println(long x) | 向writer中写入一个long基本类型变量并换行 |
25 | public void println(Object x) | 向writer中写入一个Object对象并换行 |
26 | public void println(String x) | 向writer中写入一个String对象并换行 |
示例
@SneakyThrows
public static void main(String[] args) {
List<Student> students = Lists.newArrayList();
students.add(new Student("zhuoli", 18, 99.0f));
students.add(new Student("Michael", 19, 90.0f));
students.add(new Student("Jane", 20, 60.0f));
try (PrintWriter printWriter = new PrintWriter("d:/hello.txt")) {
for (Student student : students) {
printWriter.println(student);
}
}
try (FileReader fileReader = new FileReader("d:/hello.txt");
BufferedReader bufferedReader = new BufferedReader(fileReader)) {
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
}
}
运行结果:
Student(name=zhuoli, age=18, grade=99.0)
Student(name=Michael, age=19, grade=90.0)
Student(name=Jane, age=20, grade=60.0)
参考链接:
1. Java API
2. 《Java编程的逻辑》