第十二章《文件与I/O流》第6节:字符流的使用

时间:2023-01-02 14:58:25

​计算机中的信息都是用二进制数的形式保存的,因此一组二进制数可能代表一副图片,也可能代表一段音乐。如果一组二进制数全部由字符编码组成,那么这组二进制数就代表了一段文字,人们把只有文字的信息称为“纯文本信息”或“纯文本数据”。不同的字符编码下,每个字符所占据的空间也是不同的,例如使用UTF-16编码,那么每个英文字母和汉字都占据2个字节,而如果使用UTF-8编码,每个英文字母占1个字节,而每个汉字占3个字节。字节流虽然有强大的读写功能,但它每次只能读一个字节,这样的话,无论文件采用哪一种字符编码,字节流都不能一次性读出或写入一个完整的汉字。为解决这个问题,Java语言专门定义了一组用于读写纯文本数据的流,这些流统称为“字符流”,它们的读写操作都是以字符为单位。

12.6.1 Reader类和Writer类

Reader类是所有字符输入流的父类,与InputStream一样,它也是一个抽象类。虽然它不能直接创建对象,但它所定义的方法却是其他字符流的标准操作。Reader类的各种方法都声明了IOException,因此在调用方法时要对这个异常进行处理,下面的表12-12展示了Reader类中所定义的各种方法。​

表12-12 Reader类定义的方法​

方法名

功能

abstract void close()

关闭输入

void mark(int numChars)

在输入流的指针所指位置设立一个标志。

boolean markSupported()

该流支持mark()/reset()方法

int read()

读取一个字符,如果数据源末尾返回-1

int read(char buffer[])

读取数据源中的一串字符并出入buffer数组

abstract int read(char buffer[],int offset,int numChars)

读取数据源中的一串字符并出入buffer数组下标从offset开始,长度为numChars的部分

boolean ready()

测试是否能读取下一个字符

void reset()

设置指针到先前设立的标志处

long skip(long numChars)

跳过numChars个字符,返回跳过的字符数

由于Reader类的read()方法每次读取的是一个字符,因此如果把字符读入到数组中时,数组的类型是char。从表12-12可以看出:Reader类中定义的方法与InputStream类中所定义的方法非常相似,只是读取数据的类型从字节变成了字符。​

与Reader类对应的Writer类,它用于把字符输出到数据源。Writer类也是抽象类,它是其他字符输出流的父类,下面的表12-13展示了Writer类所定义的方法。​

表12-13 Writer类定义的方法​

方法名

功能

abstract void close()

关闭输出流

abstract void flush()

缓冲中的字符强制推入目标数据源

void write(int ch)

数据源写入单个字符

void write(char buffer[ ])

数据源一个完整的字符数组

abstract void write(char buffer[ ],int offset,int numChars)

数据源数组buffer以offset开始,长度为numChars这部分数据

void write(String str)

数据源写入一个字符串

void write(String str, int offset,

int numChars)

数据源入字符串str中以offset开始,长度为numChars这部分字符

从表12-13可以看出,如果向数据源写入单个字符时,字符以int型的数据表示,而把连续的多个字符写入数据源时,这些字符可以用char数组或String字符串表示。​

12.6.2 FileReader类和FileWriter类

FileReader类用于读取纯文本文件,FileWriter类用于把字符写入纯文本文件。早期的FileReader在读取文本文件时只能以默认的字符编码规则解析读到的数据,这导致读取到的数据会出现乱码。而从JDK11开始,FileReader允许程序员在创建FileReader时通过构造方法的参数指定用哪一种字符编码规则解析读到的数据,从而避免了乱码的出现。而FileWriter则不不用考虑字符编码的问题,因为输出到文本文件中的数据都会按文件本身的设置的编码规则进行编码。此外还需要提醒读者注意:并不是所有能显示文字的文件都是纯文本文件,例如word文档,它就不属于纯文本文件,因为word文档中除了文本以外,还可以有图片,表格等内容。此外,文本文件并非只有后缀为.txt文件,任何只包含纯文本的文件都可以被称为文本文件,例如后缀为.java、.html、.xml的文件也都是文本文件。​

通常来讲,读写文件的流的构造方法都会声明FileNotFoundException,但FileReader和FileWriter的构造方法声明的却是IOException。​

FileReader类的使用方法与FileInputStream的使用方法非常相似,为帮助读者理解这两个类的不同之处,下面的【例12_09】展示了两种流读取纯文本文件的效果。为使程序能正确运行,读者需要先在D盘根目录新建一个12_09.txt,并在其中输入“你好abc”。​

【例12_09 两种流读纯文本文件】​

Exam12_09.java​

import java.io.*;
import java.nio.charset.Charset;
public class Exam12_09 {
public static void main(String[] args) {
FileInputStream fis = null;
FileReader fr1 = null;
FileReader fr2 = null;
try{
fis = new FileInputStream("D:/12_09.txt");
//指定fr1以GB2312的字符编码规则解析所读取的数据
fr1 = new FileReader("D:/12_09.txt", Charset.forName("GB2312"));
//不给fr2指定字符编码
fr2 = new FileReader("D:/12_09.txt");
int r;
System.out.print("使用fr1读文本文件中的内容:");
while ((r=fr1.read())!=-1){
System.out.print((char)r);
}
System.out.println();//换行
System.out.print("使用fr2读文本文件中的内容:");
while ((r=fr2.read())!=-1){
System.out.print((char)r);
}
System.out.println();//换行
System.out.print("使用fis读文本文件中的内容:");
while ((r=fis.read())!=-1){
System.out.print((char)r);
}
System.out.println();//换行
}catch (IOException e){
e.printStackTrace();
}finally {
if (fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fr1!=null){
try {
fr1.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fr2!=null){
try {
fr2.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

【例12_09】使用了三个流对象读取文本文件,其中fis是字节流,而fr1和fr2是字符流。创建fr1时,为其指定的字符编码是GB2312,而创建fr2时没有指定字符编码,【例12_09】的运行结果如图12-11所示。​

第十二章《文件与I/O流》第6节:字符流的使用

图12-11【例12_09】运行结果​

从图12-11可以看出:fr1能够正确解析文本文件中的数据,而fr2只能正确解析文本文件中的英文字母,无法正确解析汉字。这是因为fr1解析12_09.txt这个文件的关于汉字的字符编码规则与文件本身关于汉字的字符编码规则不同,因此出现乱码。而能够正确解析英文字母的原因是:fr2的编码规则与12_09.txt的编码规则在ASCII这一部分字符集是相同的,英文字母恰好属于ASCII,因此能够正确解析。fis虽然是一个字节流,却也能正确解析文本文件中的英文字母,这是因为英文字母在记事本文件的默认编码形式下值占一个字节,而字节流恰好能够读取一个字节,所以不会产生数据丢失的情况。​

FileWriter是用来向文本文件输出数据的流,当程序员使用这个流向文本文件输出数据时可以直接覆盖原文件的内容,也可以在原文件内容的基础上追加内容,因此FileWriter有两对构造方法,如表12-14所示。​

表12-14 FileWriter的构造方法​

构造方法

作用

FileWriter(String filePath)

创建输出数据到filePath

FileWriter(String filePath, boolean append)

创建输出数据到filePath,append为是否追加输出

FileWriter(File file)

创建输出数据到file

FileWriter(File file, boolean append)

创建输出数据到file,append为是否追加输出

在以上四个构造方法中,都是以String类或File类对象表示数据输出的目标文件,而以boolean型的append参数指定是否把文本追加到原文件的末尾,如果append的为true,则表示对象的输出方式是追加输出,否则表示覆盖输出。两个没有append参数的构造方法所创建的对象都是覆盖输出。FileWriter在调用write()方法输出单个字符时,以int型数据表示字符,因为char型数据能够自动转换为int型,所以程序员也可以直接用char型数据作为write()方法的参数。输出多个字符时,以char型数组或String表示多个字符。当输出结束后,要调用flush()方法以确保数据从缓冲区流向文本文件。下面的【例12_10】展示了如何使用FileWriter向文本文件输出数据。​

【例12_10 FileWriter类的使用】

Exam12_10.java​

import java.io.*;
public class Exam12_10 {
public static void main(String[] args) {
FileWriter fr1 = null;
FileWriter fr2 = null;
try {
fr1 = new FileWriter("D:/12_10.txt");//对象是覆盖输出
fr2 = new FileWriter("D:/12_10.txt",true);//对象是追加输出
char[] chars= {'!','!','!'};
fr1.write('A');//输出单个字符A
fr2.write(' ');//追加输出单个字符空格
fr2.write("big apple");//追加输出字符(字符串形式)
fr2.write(chars);//追加输出多个字符(字符数组形式)
fr1.flush();//把缓冲区中的数据推入文本文件
fr2.flush();//把缓冲区中的数据推入文本文件
} catch (IOException e) {
e.printStackTrace();
}finally {
if (fr1!=null){
try {
fr1.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fr2!=null){
try {
fr2.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

【例12_10】用覆盖输出和追加输出两种方式向文本文件输出数据。当运行完【例12_10】后,打开D盘可以看到出现了一个12_10.txt文件,文件的内容如图12-12所示。​

第十二章《文件与I/O流》第6节:字符流的使用

图12-12【例12_10】运行结果​

12.6.3 BufferedReader类和BufferedWriter类

FileReader一次性读取文本文件中多个字符的能力不足,因为读取多个字符时,需要用char数组作为存放字符的载体,但文件中的一行字符的长度与char数组的长度未必吻合,因此往往会出现要么一行字符要读取多次,要么一行字符无法装满char数组的情况。为解决这个问题,Java语言定义了一个BufferedReader类,它的readLine()方法能够一次性读取一行字符,这个方法的返回值是一个字符串,这个字符串就是所读到的那一行字符。在文本文件中,每一行字符都以换行作为结束,因此readLine()方法每次读到换行标志时就认为完成了一次读数据操作。如果读到整个文件的末尾,readLine()方法将会返回null。​

BufferedReader是一个处理流,它需要包装几个节点流来完成对象的创建。如果读取文本文件,那么一般会包装FileReader类的对象。为了保证能够正确解码,在创建FileReader类对象时需要指定字符编码的名称。下面的【例12_11】展示了如何使用BufferedReader以行为单位读取数据,为了能正确运行程序,读者需要在D盘根目录下新建一个12_11.txt文件,并在文件中输入一首古诗,如图12-13所示。​

第十二章《文件与I/O流》第6节:字符流的使用

图12-13 文本文件内容​

【例12_11 使用BufferedReader读取文本文件】​

Exam12_11.java​

import java.io.*;
import java.nio.charset.Charset;
public class Exam12_11 {
public static void main(String args[]) {
FileReader fr = null;
BufferedReader br = null;
try {
//创建FileReader对象并指定其字符编码为GB2312
fr = new FileReader("D:/12_11.txt", Charset.forName("GB2312"));
br = new BufferedReader(fr);
String line;
while ((line = br.readLine()) != null) {//读一行
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fr != null) {
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

【例12_11】中,BufferedReader对象包装了一个FileReader对象去读取文本文件。通过程序代码可以看出:readLine()方法能够一次性读取一行字符,并且以返回值的形式得到所读取的数据,这种读取数据的方式要比以char型数组保存数据的方式更加方便。【例12_11】的运行结果如图12-14所示。​

第十二章《文件与I/O流》第6节:字符流的使用

图12-14【例12_11】运行结果​

与BufferedReader对应的流是BufferedWriter,它定义了多个版本的write()方法,这些write()方法既能输出单个字符,也输出一个字符串,还能输出字符数组。需要注意,当使用BufferedWriter输出一个字符串时,这个输出操作不具有自动换行功能,因此如果需要完成换行操作时还需要调用newLine()方法实现。输出结束后,还需要调用flush()方法把缓冲区中的数据推入目标数据源。​

如果是把纯文本信息输出到文本文件中,一般会在创建BufferedWriter类时包装一个FileWriter类的对象,下面的【例12_12】展示了如何使用BufferedWriter类完成大段字符的输出操作。​

【例12_12使用BufferedWriter输出字符】​

Exam12_12.java​

import java.io.*;
public class Exam12_12 {
public static void main(String[] args) {
FileWriter fw = null;
BufferedWriter bw = null;
try{
fw = new FileWriter("D:/12_12.txt", true);//对文件追加输出
bw = new BufferedWriter(fw);
bw.write("大家好!");
bw.write("我正在学习BufferedWriter。");
bw.newLine();//换行
bw.write("请多多指教!");
bw.newLine();
bw.write("希望学有所成!");
bw.flush();//强制推送缓冲区数据到目的地
}catch (IOException e){
e.printStackTrace();
}finally {
if (fw != null) {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bw != null) {
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

当运行完【例12_12】后,打开D盘会发现根目录下出现了一个12_12.txt文件,其中的内容如图12-15所示。​

第十二章《文件与I/O流》第6节:字符流的使用

图12-15【例12_12】运行结果​

12.6.4 PrintStream类和PrintWriter类

输出流都定义了write()方法,write()方法在输出一个int型数据时,一般都会把这个int型数据当作字符编码,例如调用FileWriter类的write()方法向文本文件输出整数65,打开文件后会发现文本文件中有一个大写字母A,这是因为65是大写字母A的编码值,文本文件就把整数65解析成大写字母A。​

能否把一个数字“原样输出”呢?也就是说,输出的参数是65,打开文件后文件中显示的内容也是65。Java语言中定义的PrintStream类和PrintWriter类就能完成对数据的“原样输出”。在这两个类中,把数据进行原样输出的方法名称都被定义成了以单词print开头,从而与输出效果不同的write()方法相区别。​

PrintStream和PrintWriter在底层实现原理上略有不同,但它们最终的输出效果基本上是相同的。PrintStream在最早的JDK1.0中就已经定义,而PrintWriter是在JDK1.1中定义的,仅比PrintStream晚诞生了一小段时间,它相当于PrintStream的升级版,Java官方也提倡开发者在二者之间尽量使用PrintWriter来完成数据的输出,其中有一个重要原因是PrintWrite能够在创建对象时指定字符编码,从而更加灵活。实际上,开发过程中常用于输出数据的System.out就是PrintStream类的对象,介于PrintStream出现的较早,很多软件的代码已经使用了这个类,因而Java官方并没有把System.out的类型替换为PrintWriter。​

各位读者已经非常熟悉System.out的输出效果,它能把各种类型的数据打印到控制台,实际上,PrintStream和PrintWriter能够把数据输出到任何类型的目标数据源,例如文件、网络等。此外,输出方式也很多样化,例如有不换行的print()方法、能够自动换行的println()方法以及能够控制输出内容宽度的printf()方法。由于PrintStream和PrintWriter的输出效果相同,下面的【例12_13】仅以PrintWriter展示如何实现数据原样输出到文件的效果。​

【例12_13使用PrintWriter输出数据】​

Exam12_13.java​

import java.io.*;
public class Exam12_13 {
public static void main(String[] args) {
PrintWriter pw = null;
try {
pw = new PrintWriter("D:/12_13.txt");
pw.print("输出boolean型数据:");
pw.println(true);
pw.print("输出double型数据:");
pw.println(3.14);
pw.print("控制输出数据宽度(8个字符):");
pw.printf("%-8s%-8s%-8s","张三","李四","王五");
pw.flush();
}catch (FileNotFoundException e){
e.printStackTrace();
}finally {
if (pw!=null){
pw.close();
}
}
}
}

如果把PrintWriter的目标数据源定位于一个文件,那么创建PrintWriter类对象时要处理表示无法找到文件的FileNotFoundException,而PrintWriter类对象的各种方法都没有声明抛出异常,因此调用这些方法时无需对异常进行处理,例如本例中close()方法的调用就没有处理任何异常。运行【例12_13】后,打开D盘的12_13.txt文件,可以看到如图12-16所示的内容。​

第十二章《文件与I/O流》第6节:字符流的使用

图12-16【例12_13运行结果】​

读者通过这个例子可以体验到:把数据输出到文本文件的效果实际上与输出到控制台上的效果是相同的。​

12.6.5 InputStreamReader类和OutputStreamWriter类

如果一个字节流的数据源被设定为一个文本文件,那么使用字节流去读写这个文本文件就非常不方便,这是因为在文本文件中一个汉字至少占2个字节,而字节流每次只能读写一个字节。在无法改变字节流数据源的情况下,可以使用InputStreamReader类和OutputStreamWriter类来对字节流进行包装,经过包装之后,使用字节流也能读写文本文件。实际上,InputStreamReader类和OutputStreamWriter类不仅能够包装读写文件的字节流FileInputStream和FileOutputStream,它们可以包装任何类型的字节流。​

InputStreamReader和OutputStreamWriter属于字符流,因此它们可以像其他字符流那样读写数据。下面的【例12_14】展示了如何使用InputStreamReader包装字节流并读取文本文件。为了程序能够正确运行,读者需要先在D盘根目录下新建一个12_14.txt,并在其中输入“我正在学Java”。​

【例12_14 InputStreamReader类的使用】

Exam12_14.java​

import java.io.*;
import java.nio.charset.Charset;
public class Exam12_14 {
public static void main(String[] args) {
FileInputStream fis1 = null;
FileInputStream fis2 = null;
InputStreamReader isr = null;
try{
fis1 = new FileInputStream("D:/12_14.txt");
fis2 = new FileInputStream("D:/12_14.txt");
isr = new InputStreamReader(fis2, Charset.forName("GB2312"));
int r;
System.out.print("直接使用FileInputStream读文件:");
while ((r=fis1.read())!=-1){
System.out.print((char)r);
}
System.out.println();//换行
System.out.print("使用InputStreamReader读文件:");
while ((r=isr.read())!=-1){
System.out.print((char)r);
}
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}finally {
if(fis1!=null){
try {
fis1.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fis2!=null){
try {
fis2.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(isr!=null){
try {
isr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

为了对比读数据的效果,【例12_14】使用FileInputSteam和InputSteamReader两个流来读取文本文件。之所以要创建两个FileInputSteam类对象fis1和fis2,是因为fis1在读完文件之后,指针已经到达文件末尾,如果用isr包装fis1,那么isr就无法读取数据,因此要用isr包装一个没有读过文件的fis2对象才能完整的读取文件内容。【例12_14】的运行结果如图12-17所示。​

第十二章《文件与I/O流》第6节:字符流的使用

图12-17【例12_14】运行结果​

从图12-14可以看出:FileInputStream类对象只能正确的读取文本文件中的字母,无法正确读取汉字,而经过包装之后,使用InputStreamReader则能正确读取所有字符。​

OutputStreamWriter能够包装字节流,这样就能让程序员可以用字符流的方式操作文本文件。下面的【例12_15】展示了OutputStreamWriter的用法。​

【例12_15 OutputStreamWriter类的使用】

Exam12_15.java​

import java.io.*;
import java.nio.charset.Charset;
public class Exam12_15 {
public static void main(String[] args) {
FileOutputStream fos1 = null;
FileOutputStream fos2 = null;
OutputStreamWriter osr = null;
try{
fos1 = new FileOutputStream("D:/12_15.txt");
fos2 = new FileOutputStream("D:/12_15.txt",true);//追加输出
osr = new OutputStreamWriter(fos2,Charset.forName("GB2312"));
String str = "我喜欢Java";
char[] chars = str.toCharArray();
//使用FileOutputStream输出字符
for (int i=0;i<chars.length;i++){
fos1.write(chars[i]);//逐个输出字符
}
//使用OutputStreamWriter输出字符
osr.write("\r\n");//在文件中换行
osr.write(chars);
osr.flush();
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}finally {
if(fos1!=null){
try {
fos1.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fos2!=null){
try {
fos2.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(osr!=null){
try {
osr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

运行【例12_15】后,D盘会出现12_15.txt文件,文件的内容如图12-18所示。​

第十二章《文件与I/O流》第6节:字符流的使用

图12-18【例12_15】运行结果​

从图12-18可以看出,使用FileOutputStream只能正确输出英文字母,无法正确输出汉字,但经过OutputStreamWriter包装后,可以正确输出任何字符。

本文字版教程还配有更详细的视频讲解,小伙伴们可以点击这里观看。