13.3 文本文件和字符流
13.3 文本文件和字符流
上节介绍了如何以字节流的方式处理文件,对于文本文件,字节流没有编码的概念,不能按行处理,使用不太方便,更适合的是使用字符流,本节就来介绍字符流。
我们首先简要介绍文本文件的基本概念、与二进制文件的区别、编码,以及字符流和字节流的区别,然后介绍Java中的主要字符流,它们有:
1)Reader/Writer:字符流的基类,它们是抽象类;
2)InputStreamReader/OutputStreamWriter:适配器类,将字节流转换为字符流;
3)FileReader/FileWriter:输入源和输出目标是文件的字符流;
4)CharArrayReader/CharArrayWriter:输入源和输出目标是char数组的字符流;
5)StringReader/StringWriter:输入源和输出目标是String的字符流;
6)BufferedReader/BufferedWriter:装饰类,对输入/输出流提供缓冲,以及按行读写功能;
7)PrintWriter:装饰类,可将基本类型和对象转换为其字符串形式输出的类。
除了这些类,Java中还有一个类Scanner,类似于一个Reader,但不是Reader的子类,可以读取基本类型的字符串形式,类似于PrintWriter的逆操作。理解了字节流和字符流后,我们介绍Java中的标准输入输出和错误流。最后,我们总结一些简单的实用方法。
13.3.1 基本概念
我们先来看一些基本概念,包括文本文件、编码和字符流。
1.文本文件
上节提到,处理文件要有二进制思维。从二进制角度,我们通过一个简单的例子解释下文本文件与二进制文件的区别。比如,要存储整数123,使用二进制形式保存到文件test. dat,代码为:
1 | DataOutputStream output = new DataOutputStream( |
使用UltraEdit打开该文件,显示的却是:
1 | { |
打开十六进制编辑器,显示如图13-3所示。
在文件中存储的实际有4个字节,最低位字节7B对应的十进制数是123,也就是说,对int类型,二进制文件保存的直接就是int的二进制形式。这个二进制形式,如果当成字符来解释,显示成什么字符则与编码有关,如果当成UTF-32BE编码,解释成的就是一个字符,即{。
如果使用文本文件保存整数123,则代码为:
1 | OutputStream output = new FileOutputStream("test.txt"); |
代码将整数123转换为字符串,然后将它的UTF-8编码输出到了文件中,使用Ultra-Edit打开该文件,显示的就是期望的:
1 | 123 |
打开十六进制编辑器,显示如图13-4所示。
文件中实际存储的有三个字节:31、32、33,对应的十进制数分别是49、50、51,分别对应字符’1’、’2’、’3’的ASCII编码。
2.编码
在文本文件中,编码非常重要,同一个字符,不同编码方式对应的二进制形式可能是不一样的。我们看个例子,对同样的文本:
1 | hello, 123, 老马 |
1)UTF-8编码,十六进制如图13-5所示。
英文和数字字符每个占一个字节,而每个中文占三个字节。
2)GB18030编码,十六进制如图13-6所示。
英文和数字字符与UTF-8编码是一样的,但中文不一样,每个中文占两个字节。
3)UTF-16BE编码,十六进制为如图13-7所示。
无论是英文还是中文字符,每个字符都占两个字节。UTF-16BE也是Java内存中对字符的编码方式。
3.字符流
字节流是按字节读取的,而字符流则是按char读取的,一个char在文件中保存的是几个字节与编码有关,但字符流封装了这种细节,我们操作的对象就是char。
需要说明的是,一个char不完全等同于一个字符,对于绝大部分字符,一个字符就是一个char,但我们之前介绍过,对于增补字符集中的字符,需要两个char表示,对于这种字符,Java中的字符流是按char而不是一个完整字符处理的。
理解了文本文件、编码和字符流的概念,我们再来看Java中的相关类,从基类开始。
13.3.2 Reader/Writer
Reader与字节流的InputStream类似,也是抽象类,部分主要方法有:
1 | public int read() throws IOException |
方法的名称和含义与InputStream中的对应方法基本类似,但Reader中处理的单位是char,比如read读取的是一个char,取值范围为0~65 535。Reader没有available方法,对应的方法是ready()。
Writer与字节流的OutputStream类似,也是抽象类,部分主要方法有:
1 | public void write(int c) |
含义与OutputStream的对应方法基本类似,但Writer处理的单位是char, Writer还接受String类型,我们知道,String的内部就是char数组,处理时,会调用String的getChar方法先获取char数组。
13.3.3 InputStreamReader/OutputStreamWriter
InputStreamReader和OutputStreamWriter是适配器类,能将InputStream/OutputStream转换为Reader/Writer。
1. OutputStreamWriter
OutputStreamWriter的主要构造方法为:
1 | public OutputStreamWriter(OutputStream out) |
一个重要的参数是编码类型,可以通过名字charsetName或Charset对象传入,如果没有传入,则为系统默认编码,默认编码可以通过Charset.defaultCharset()得到。Output-StreamWriter内部有一个类型为StreamEncoder的编码器,能将char转换为对应编码的字节。
我们看一段简单的代码,将字符串”hello, 123,老马”写到文件hello.txt中,编码格式为GB2312:
1 | Writer writer = new OutputStreamWriter( |
创建一个FileOutputStream,然后将其包在一个OutputStreamWriter中,就可以直接以字符串写入了。
2. InputStreamReader
InputStreamReader的主要构造方法为:
1 | public InputStreamReader(InputStream in) |
与OutputStreamWriter一样,一个重要的参数是编码类型。InputStreamReader内部有一个类型为StreamDecoder的解码器,能将字节根据编码转换为char。
我们看一段简单的代码,将上面写入的文件读进来:
1 | Reader reader = new InputStreamReader( |
这段代码假定一次read调用就读到了所有内容,且假定长度不超过1024。为了确保读到所有内容,可以借助待会介绍的CharArrayWriter或StringWriter。
13.3.4 FileReader/FileWriter
FileReader/FileWriter的输入和目的是文件。FileReader是InputStreamReader的子类,它的主要构造方法有:
1 | public FileReader(File file) throws FileNotFoundException |
FileWriter是OutputStreamWriter的子类,它的主要构造方法有:
1 | public FileWriter(File file) throws IOException |
append参数指定是追加还是覆盖,如果没传,则为覆盖。
需要注意的是,FileReader/FileWriter不能指定编码类型,只能使用默认编码,如果需要指定编码类型,可以使用InputStreamReader/OutputStreamWriter。
13.3.5 CharArrayReader/CharArrayWriter
CharArrayWriter与ByteArrayOutputStream类似,它的输出目标是char数组,这个数组的长度可以根据数据内容动态扩展。
CharArrayWriter有如下方法,可以方便地将数据转换为char数组或字符串:
1 | public char[] toCharArray() |
使用CharArrayWriter,我们可以改进上面的读文件代码,确保将所有文件内容读入:
1 | Reader reader = new InputStreamReader( |
读入的数据先写入CharArrayWriter中,读完后,再调用其toString()方法获取完整数据。
CharArrayReader与上节介绍的ByteArrayInputStream类似,它将char数组包装为一个Reader,是一种适配器模式,它的构造方法有:
1 | public CharArrayReader(char buf[]) |
13.3.6 StringReader/StringWriter
StringReader/StringWriter与CharArrayReader/CharArrayWriter类似,只是输入源为String,输出目标为StringBuffer,而且,String/StringBuffer内部是由char数组组成的,所以它们本质上是一样的,具体我们就不赘述了。之所以要将char数组和String与Reader/Writer进行转换,也是为了能够方便地参与Reader/Writer构成的协作体系,复用代码。
13.3.7 BufferedReader/BufferedWriter
BufferedReader/BufferedWriter是装饰类,提供缓冲,以及按行读写功能。Buffered-Writer的构造方法有:
1 | public BufferedWriter(Writer out) |
参数sz是缓冲大小,如果没有提供,默认为8192。它有如下方法,可以输出平台特定的换行符:
1 | public void newLine() throws IOException |
BufferedReader的构造方法有:
1 | public BufferedReader(Reader in) |
参数sz是缓冲大小,如果没有提供,默认为8192。它有如下方法,可以读入一行:
1 | public String readLine() throws IOException |
字符’\r’或’\n’或’\r\n’被视为换行符,readLine返回一行内容,但不会包含换行符,当读到流结尾时,返回null。
FileReader/FileWriter是没有缓冲的,也不能按行读写,所以,一般应该在它们的外面包上对应的缓冲类。我们来看个例子,还是学生列表,这次我们使用可读的文本进行保存,一行保存一条学生信息,学生字段之间用逗号分隔,保存的代码为:
1 | public static void writeStudents(List<Student> students) throws IOException{ |
保存后的文件内容显示为:
1 | 张三,18,80.9 |
从文件中读取的代码为:
1 | public static List<Student> readStudents() throws IOException{ |
使用readLine读入每一行,然后使用String的方法分隔字段,再调用Integer和Double的方法将字符串转换为int和double。这种对每一行的解析可以使用类Scanner进行简化,待会我们介绍。
13.3.8 PrintWriter
PrintWriter有很多重载的print方法,如:
1 | public void print(int i) |
它会将这些参数转换为其字符串形式,即调用String.valueOf(),然后再调用write。它也有很多重载形式的println方法,println除了调用对应的print,还会输出一个换行符。除此之外,PrintWriter还有格式化输出方法,如:
1 | public PrintWriter printf(String format, Object ... args) |
format表示格式化形式,比如,保留小数点后两位,格式可以为:
1 | PrintWriter writer = … |
输出为:
1 | 123.45 |
更多格式化的内容可以参看API文档,本节就不赘述了。
PrintWriter的方便之处在于,它有很多构造方法,可以接受文件路径名、文件对象、OutputStream、Writer等,对于文件路径名和File对象,还可以接受编码类型作为参数,比如:
1 | public PrintWriter(File file) throws FileNotFoundException |
参数csn表示编码类型,对于以文件对象和文件名为参数的构造方法,PrintWriter内部会构造一个BufferedWriter,比如:
1 | public PrintWriter(String fileName) throws FileNotFoundException { |
对于以OutputSream为参数的构造方法,PrintWriter也会构造一个BufferedWriter,比如:
1 | public PrintWriter(OutputStream out, boolean autoFlush) { |
对于以Writer为参数的构造方法,PrintWriter就不会包装BufferedWriter了。
构造方法中的autoFlush参数表示同步缓冲区的时机,如果为true,则在调用println、printf或format方法的时候,同步缓冲区,如果没有传,则不会自动同步,需要根据情况调用flush方法。
可以看出,PrintWriter是一个非常方便的类,可以直接指定文件名作为参数,可以指定编码类型,可以自动缓冲,可以自动将多种类型转换为字符串,在输出到文件时,可以优先选择该类。
上面的保存学生列表代码,使用PrintWriter,可以写为:
1 | public static void writeStudents(List<Student> students) throws IOException{ |
PrintWriter有一个非常相似的类PrintStream,除了不能接受Writer作为构造方法外, PrintStream的其他构造方法与PrintWriter一样。PrintStream也有几乎一样的重载的print和println方法,只是自动同步缓冲区的时机略有不同,在PrintStream中,只要碰到一个换行字符’\n’,就会自动同步缓冲区。PrintStream与PrintWriter的另一个区别是,虽然它们都有如下方法:
1 | public void write(int b) |
但含义是不一样的,PrintStream只使用最低的8位,输出一个字节,而PrintWriter是使用最低的两位,输出一个char。
13.3.9 Scanner
Scanner是一个单独的类,它是一个简单的文本扫描器,能够分析基本类型和字符串,它需要一个分隔符来将不同数据区分开来,默认是使用空白符,可以通过useDelimiter()方法进行指定。Scanner有很多形式的next()方法,可以读取下一个基本类型或行,如:
1 | public float nextFloat() |
Scanner也有很多构造方法,可以接受File对象、InputStream、Reader作为参数,它也可以将字符串作为参数,这时,它会创建一个StringReader。比如,以前面的解析学生记录为例,使用Scanner,代码可以改为:
1 | public static List<Student> readStudents() throws IOException{ |
13.3.10 标准流
我们之前一直在使用System.out向屏幕上输出,它是一个PrintStream对象,输出目标就是所谓的“标准”输出,经常是屏幕。除了System.out, Java中还有两个标准流:System. in和System.err。
System.in表示标准输入,它是一个InputStream对象,输入源经常是键盘。比如,从键盘接受一个整数并输出,代码可以为:
1 | Scanner in = new Scanner(System.in); |
System.err表示标准错误流,一般异常和错误信息输出到这个流,它也是一个Print-Stream对象,输出目标默认与System.out一样,一般也是屏幕。
标准流的一个重要特点是,它们可以重定向,比如可以重定向到文件,从文件中接受输入,输出也写到文件中。在Java中,可以使用System类的setIn、setOut、setErr进行重定向,比如:
1 | System.setIn(new ByteArrayInputStream("hello".getBytes("UTF-8"))); |
标准输入重定向到了一个ByteArrayInputStream,标准输出和错误重定向到了文件,所以第一次调用in.nextLine就会读取到”hello”,输出文件out.txt中也包含该字符串,第二次调用in.nextLine会触发异常,异常消息会写到错误流中,即文件err.txt中会包含异常消息,为”No line found”。
在实际开发中,经常需要重定向标准流。比如,在一些自动化程序中,经常需要重定向标准输入流,以从文件中接受参数,自动执行,避免人手工输入。在后台运行的程序中,一般都需要重定向标准输出和错误流到日志文件,以记录和分析运行的状态和问题。
在Linux系统中,标准输入输出流也是一种重要的协作机制。很多命令都很小,只完成单一功能,实际完成一项工作经常需要组合使用多条命令,它们协作的模式就是通过标准输入输出流,每个命令都可以从标准输入接受参数,处理结果写到标准输出,这个标准输出可以连接到下一个命令作为标准输入,构成管道式的处理链条。比如,查找一个日志文件access.log中127.0.0.1出现的行数,可以使用命令:
1 | cat access.log | grep 127.0.0.1 | wc -l |
有三个程序cat、grep、wc, |是管道符号,它将cat的标准输出重定向为了grep的标准输入,而grep的标准输出又成了wc的标准输入。
13.3.11 实用方法
可以看出,字符流也包含了很多的类,虽然很灵活,但对于一些简单的需求,却需要写很多代码,实际开发中,经常需要将一些常用功能进行封装,提供更为简单的接口。下面我们提供一些实用方法,以供参考,代码比较简单,就不解释了。
复制Reader到Writer,代码为:
1 | public static void copy(final Reader input, |
将文件全部内容读入到一个字符串,参数为文件名和编码类型,代码为:
1 | public static String readFileToString(final String fileName, |
这个方法利用了StringWriter,并调用了上面的复制方法。
将字符串写到文件,参数为文件名、字符串内容和编码类型,代码为:
1 | public static void writeStringToFile(final String fileName, |
按行将多行数据写到文件,参数为文件名、编码类型、行的集合,代码为:
1 | public static void writeLines(final String fileName, final String encoding, |
按行将文件内容读到一个列表中,参数为文件名、编码类型,代码为:
1 | public static List<String> readLines(final String fileName, |
13.3.12 小结
本节介绍了如何在Java中以字符流的方式读写文本文件,我们强调了二进制思维、文本文本与二进制文件的区别、编码,以及字符流与字节流的不同,介绍了个各种字符流、Scanner以及标准流,最后总结了一些实用方法。完整的代码在github上,地址为https://github.com/swiftma/program-logic ,位于包shuo.laoma.file.c58下。
写文件时,可以优先考虑PrintWriter,因为它使用方便,支持自动缓冲、指定编码类型、类型转换等。读文件时,如果需要指定编码类型,需要使用InputStreamReader;如果不需要指定编码类型,可使用FileReader,但都应该考虑在外面包上缓冲类Buffered-Reader。
通过前面两个小节,我们应该可以从容地读写文件内容了,但文件和目录本身的操作,如查看元数据信息、文件重命名、遍历文件、查找文件、新建目录等,又该如何进行呢?让我们下节介绍。