计算机程序的思惟逻辑 (58) - 文本文件和字符流

本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》(马俊昌著),由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买:京东自营连接 html

上节咱们介绍了如何以字节流的方式处理文件,咱们提到,对于文本文件,字节流没有编码的概念,不能按行处理,使用不太方便,更适合的是使用字符流,本节就来介绍字符流。java

咱们首先简要介绍下文本文件的基本概念、与二进制文件的区别、编码、以及字符流和字节流的区别,而后咱们介绍Java中的主要字符流,它们有:编程

  • Reader/Writer:字符流的基类,它们是抽象类。
  • InputStreamReader/OutputStreamWriter:适配器类,输入是InputStream,输出是OutputStream,将字节流转换为字符流。
  • FileReader/FileWriter:输入源和输出目标是文件的字符流。
  • CharArrayReader/CharArrayWriter: 输入源和输出目标是char数组的字符流。
  • StringReader/StringWriter:输入源和输出目标是String的字符流。
  • BufferedReader/BufferedWriter:装饰类,对输入输出流提供缓冲,以及按行读写功能。
  • PrintWriter:装饰类,可将基本类型和对象转换为其字符串形式输出的类。

除了这些类,Java中还有一个类Scanner,相似于一个Reader,但不是Reader的子类,能够读取基本类型的字符串形式,相似于PrintWriter的逆操做。数组

理解了字节流和字符流后,咱们介绍一下Java中的标准输入输出和错误流。bash

最后,咱们总结一些简单的实用方法。微信

基本概念

文本文件

上节咱们提到,处理文件要有二进制思惟。从二进制角度,咱们经过一个简单的例子解释下文本文件与二进制文件的区别,好比说要存储整数123,使用二进制形式保存到文件test.dat,代码为:app

DataOutputStream output = new DataOutputStream(new FileOutputStream("test.dat"));
try{
    output.writeInt(123);
}finally{
    output.close();
}
复制代码

使用UltraEdit打开该文件,显示的倒是:编辑器

{                        
复制代码

打开十六进制编辑器,显示的为:ui

在文件中存储的实际有四个字节,最低位字节7B对应的十进制数是123,也就是说,对int类型,二进制文件保存的直接就是int的二进制形式。这个二进制形式,若是当成字符来解释,显示成什么字符则与编码有关,若是当成UTF-32BE编码,解释成的就是一个字符,即{。

若是使用文本文件保存整数123,则代码为:this

OutputStream output = new FileOutputStream("test.txt");
try{
    String data = Integer.toString(123);
    output.write(data.getBytes("UTF-8"));
}finally{
    output.close();
}
复制代码

代码将整数123转换为字符串,而后将它的UTF-8编码输出到了文件中,使用UltraEdit打开该文件,显示的就是指望的:

123
复制代码

打开十六进制编辑器,显示的为:

文件中实际存储的有三个字节,31 32 33对应的十进制数分别是49 50 51,分别对应字符'1','2','3'的ASCII编码。

编码

在文本文件中,编码很是重要,同一个字符,不一样编码方式对应的二进制形式多是不同的,咱们看个例子,对一样的文本:

hello, 123, 老马
复制代码

UTF-8编码,十六进制为:

英文和数字字符每一个占一个字节,而每一个中文占三个字节。

GB18030编码,十六进制为:

英文和数字字符与UTF-8编码是同样的,但中文不同,每一个中文占两个字节。

UTF-16BE编码,十六进制为:

不管是英文仍是中文字符,每一个字符都占两个字节。UTF-16BE也是Java内存中对字符的编码方式。

字符流

字节流是按字节读取的,而字符流则是按char读取的,一个char在文件中保存的是几个字节与编码有关,但字符流给咱们封装了这种细节,咱们操做的对象就是char。

须要说明的是,一个char不彻底等同于一个字符,对于绝大部分字符,一个字符就是一个char,但咱们以前介绍过,对于增补字符集中的字符,好比'💎',它须要两个char表示,对于这种字符,Java中的字符流是按char而不是一个完整字符处理的。

理解了文本文件、编码和字符流的概念,咱们再来看Java中的相关类,从基类开始。

Reader/Writer

Reader

Reader与字节流的InputStream相似,也是抽象类,主要有以下方法:

public int read() throws IOException public int read(char cbuf[]) throws IOException abstract public int read(char cbuf[], int off, int len) throws IOException;
abstract public void close() throws IOException;
public long skip(long n) throws IOException public boolean markSupported() public void mark(int readAheadLimit) throws IOException public void reset() throws IOException public boolean ready() throws IOException 复制代码

方法的名称和含义与InputStream中的对应方法基本相似,但Reader中处理的单位是char,好比read读取的是一个char,取值范围为0到65535。Reader没有available方法,对应的方法是ready()。

Writer

Writer与字节流的OutputStream相似,也是抽象类,主要有以下方法:

public void write(int c) public void write(char cbuf[]) abstract public void write(char cbuf[], int off, int len) throws IOException;
public void write(String str) throws IOException public void write(String str, int off, int len) abstract public void close() throws IOException;
abstract public void flush() throws IOException;
复制代码

含义与OutputStream的对应方法基本相似,但Writer处理的单位是char,Writer还接受String类型,咱们知道,String的内部就是char数组,处理时,会调用String的getChar方法先获取char数组。

InputStreamReader/OutputStreamWriter

InputStreamReader和OutputStreamWriter是适配器类,能将InputStream/OutputStream转换为Reader/Writer。

OutputStreamWriter

OutputStreamWriter的主要构造方法为:

public OutputStreamWriter(OutputStream out) public OutputStreamWriter(OutputStream out, String charsetName) public OutputStreamWriter(OutputStream out, Charset cs) 复制代码

一个重要的参数是编码类型,能够经过名字charsetName或Charset对象传入,若是没有传,则为系统默认编码,默认编码能够经过Charset.defaultCharset()获得。OutputStreamWriter内部有一个类型为StreamEncoder的编码器,能将char转换为对应编码的字节。

咱们看一段简单的代码,将字符串"hello, 123, 老马"写到文件hello.txt中,编码格式为GB2312:

Writer writer = new OutputStreamWriter(
        new FileOutputStream("hello.txt"), "GB2312");
try{
    String str = "hello, 123, 老马";
    writer.write(str);
}finally{
    writer.close();
}
复制代码

建立一个FileOutputStream,而后将其包在一个OutputStreamWriter中,就能够直接以字符串写入了。

InputStreamReader

InputStreamReader的主要构造方法为:

public InputStreamReader(InputStream in) public InputStreamReader(InputStream in, String charsetName) public InputStreamReader(InputStream in, Charset cs) 复制代码

与OutputStreamWriter同样,一个重要的参数是编码类型。InputStreamReader内部有一个类型为StreamDecoder的解码器,能将字节根据编码转换为char。

咱们看一段简单的代码,将上面写入的文件读进来:

Reader reader = new InputStreamReader(
        new FileInputStream("hello.txt"), "GB2312");
try{
    char[] cbuf = new char[1024];
    int charsRead = reader.read(cbuf);
    System.out.println(new String(cbuf, 0, charsRead));
}finally{
    reader.close();
}
复制代码

这段代码假定一次read调用就读到了全部内容,且假定长度不超过1024。为了确保读到全部内容,能够借助待会介绍的CharArrayWriter或StringWriter。

FileReader/FileWriter

FileReader/FileWriter的输入和目的是文件。FileReader是InputStreamReader的子类,它的主要构造方法有:

public FileReader(File file) throws FileNotFoundException public FileReader(String fileName) throws FileNotFoundException 复制代码

FileWriter是OutputStreamWriter的子类,它的主要构造方法有:

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 复制代码

append参数指定是追加仍是覆盖,若是没传,为覆盖。

须要注意的是,FileReader/FileWriter不能指定编码类型,只能使用默认编码,若是须要指定编码类型,可使用InputStreamReader/OutputStreamWriter。

CharArrayReader/CharArrayWriter

CharArrayWriter

CharArrayWriter与ByteArrayOutputStream相似,它的输出目标是char数组,这个数组的长度能够根据数据内容动态扩展。

CharArrayWriter有以下方法,能够方便的将数据转换为char数组或字符串:

public char[] toCharArray()
public String toString() 复制代码

使用CharArrayWriter,咱们能够改进上面的读文件代码,确保将全部文件内容读入:

Reader reader = new InputStreamReader(
        new FileInputStream("hello.txt"), "GB2312");
try{
    CharArrayWriter writer = new CharArrayWriter();
    char[] cbuf = new char[1024];
    int charsRead = 0;
    while((charsRead=reader.read(cbuf))!=-1){
        writer.write(cbuf, 0, charsRead);
    }
    System.out.println(writer.toString());
}finally{
    reader.close();
}
复制代码

读入的数据先写入CharArrayWriter中,读完后,再调用其toString方法获取完整数据。

CharArrayReader

CharArrayReader与上节介绍的ByteArrayInputStream相似,它将char数组包装为一个Reader,是一种适配器模式,它的构造方法有:

public CharArrayReader(char buf[]) public CharArrayReader(char buf[], int offset, int length) 复制代码

StringReader/StringWriter

StringReader/StringWriter与CharArrayReader/CharArrayWriter相似,只是输入源为String,输出目标为StringBuffer,并且,String/StringBuffer内部是由char数组组成的,因此它们本质上是同样的。

之因此要将char数组/String与Reader/Writer进行转换也是为了可以方便的参与Reader/Writer构成的协做体系,复用代码。

BufferedReader/BufferedWriter

BufferedReader/BufferedWriter是装饰类,提供缓冲,以及按行读写功能。BufferedWriter的构造方法有:

public BufferedWriter(Writer out) public BufferedWriter(Writer out, int sz) 复制代码

参数sz是缓冲大小,若是没有提供,默认为8192。它有以下方法,能够输出平台特定的换行符:

public void newLine() throws IOException 复制代码

BufferedReader的构造方法有:

public BufferedReader(Reader in) public BufferedReader(Reader in, int sz) 复制代码

参数sz是缓冲大小,若是没有提供,默认为8192。它有以下方法,能够读入一行:

public String readLine() throws IOException 复制代码

字符'\r'或'\n'或'\r\n'被视为换行符,readLine返回一行内容,但不会包含换行符,当读到流结尾时,返回null。

FileReader/FileWriter是没有缓冲的,也不能按行读写,因此,通常应该在它们的外面包上对应的缓冲类。

咱们来看个例子,仍是上节介绍的学生列表,此次咱们使用可读的文本进行保存,一行保存一条学生信息,学生字段之间用逗号分隔,保存的代码为:

public static void writeStudents(List<Student> students) throws IOException{
    BufferedWriter writer = null;
    try{
        writer = new BufferedWriter(new FileWriter("students.txt"));
        for(Student s : students){
            writer.write(s.getName()+","+s.getAge()+","+s.getScore());
            writer.newLine();
        }
    }finally{
        if(writer!=null){
            writer.close();    
        }
    }
}
复制代码

保存后的文件内容显示为:

张三,18,80.9
李四,17,67.5
```
从文件中读取的代码为:
```java
public static List<Student> readStudents() throws IOException{
    BufferedReader reader = null;
    try{
        reader = new BufferedReader(
                new FileReader("students.txt"));
        List<Student> students = new ArrayList<>();
        String line = reader.readLine();
        while(line!=null){
            String[] fields = line.split(",");
            Student s = new Student();
            s.setName(fields[0]);
            s.setAge(Integer.parseInt(fields[1]));
            s.setScore(Double.parseDouble(fields[2]));
            students.add(s);
            line = reader.readLine();
        }
        return students;
    }finally{
        if(reader!=null){
            reader.close();
        }
    }
}
```
使用readLine读入每一行,而后使用String的方法分隔字段,再调用Integer和Double的方法将字符串转换为int和double,这种对每一行的解析可使用类Scanner进行简化,待会咱们介绍。

## PrintWriter

PrintWriter有不少重载的print方法,如:
```java
public void print(int i)
public void print(long l)
public void print(double d)
public void print(Object obj)
```
它会将这些参数转换为其字符串形式,即调用String.valueOf(),而后再调用write。它也有不少重载形式的println方法,println除了调用对应的print,还会输出一个换行符。除此以外,PrintWriter还有格式化输出方法,如:
```java
public PrintWriter printf(String format, Object ... args)
```
format表示格式化形式,好比,保留小数点后两位,格式能够为:
```java
PrintWriter writer = ...
writer.format("%.2f", 123.456f);
```
输出为:
```
123.45
```
更多格式化的内容能够参看Java文档,本文就不赘述了。

PrintWriter的方便之处在于,它有不少构造方法,能够接受文件路径名、文件对象、OutputStream、Writer等,对于文件路径名和File对象,还能够接受编码类型做为参数,以下所示:
```java
public PrintWriter(File file) throws FileNotFoundException
public PrintWriter(File file, String csn)
public PrintWriter(String fileName) throws FileNotFoundException
public PrintWriter(String fileName, String csn)
public PrintWriter(OutputStream out)
public PrintWriter(OutputStream out, boolean autoFlush)
public PrintWriter (Writer out)
public PrintWriter(Writer out, boolean autoFlush)
```
参数csn表示编码类型,对于以文件对象和文件名为参数的构造方法,PrintWriter内部会构造一个BufferedWriter,好比:
```java
public PrintWriter(String fileName) throws FileNotFoundException {
    this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName))),
         false);
}
```
对于以OutputSream为参数的构造方法,PrintWriter也会构造一个BufferedWriter,好比:
```java
public PrintWriter(OutputStream out, boolean autoFlush) {
    this(new BufferedWriter(new OutputStreamWriter(out)), autoFlush);
    ...
}
```
对于以Writer为参数的构造方法,PrintWriter就不会包装BufferedWriter了。

构造方法中的autoFlush参数表示同步缓冲区的时机,若是为true,则在调用println, printf或format方法的时候,同步缓冲区,若是没有传,则不会自动同步,须要根据状况调用flush方法。

能够看出,<span style="color:blue">PrintWriter是一个很是方便的类,能够直接指定文件名做为参数,能够指定编码类型,能够自动缓冲,能够自动将多种类型转换为字符串,在输出到文件时,能够优先选择该类。</span>

上面的保存学生列表代码,使用PrintWriter,能够写为:
```java
public static void writeStudents(List<Student> students) throws IOException{
    PrintWriter writer = new PrintWriter("students.txt");
    try{
        for(Student s : students){
            writer.println(s.getName()+","+s.getAge()+","+s.getScore());
        }
    }finally{
        writer.close();
    }
}
```
PrintWriter有一个很是类似的类PrintStream,除了不能接受Writer做为构造方法外,PrintStream的其余构造方法与PrintWriter同样,PrintStream也有几乎同样的重载的print和println方法,只是自动同步缓冲区的时机略有不一样,在PrintStream中,只要碰到一个换行字符'\n',就会自动同步缓冲区。

PrintStream与PrintWriter的另外一个区别是,虽然它们都有以下方法:
```java
public void write(int b)
```
但含义是不同的,PrintStream只使用最低的八位,输出一个字节,而PrintWriter是使用最低的两位,输出一个char。

## Scanner

Scanner是一个单独的类,它是一个简单的文本扫描器,可以分析基本类型和字符串,它须要一个分隔符来将不一样数据区分开来,默认是使用空白符,能够经过useDelimiter方法进行指定。Scanner有不少形式的next方法,能够读取下一个基本类型或行,如:
```java
public float nextFloat()
public int nextInt()
public String nextLine()
```
Scanner也有不少构造方法,能够接受File对象、InputStream、Reader做为参数,它也能够将字符串做为参数,这时,它会建立一个StringReader,好比,之前面的解析学生记录为例,使用Scanner,代码能够改成:
```java
public static List<Student> readStudents() throws IOException{
    BufferedReader reader = new BufferedReader(
            new FileReader("students.txt"));
    try{
        List<Student> students = new ArrayList<Student>();
        String line = reader.readLine();
        while(line!=null){
            Student s = new Student();
            Scanner scanner = new Scanner(line).useDelimiter(",");
            s.setName(scanner.next());
            s.setAge(scanner.nextInt());
            s.setScore(scanner.nextDouble());
            students.add(s);
            line = reader.readLine();
        }
        return students;
    }finally{
        reader.close();
    }
}
```
## 标准流

咱们以前一直在使用System.out向屏幕上输出,它是一个PrintStream对象,输出目标就是所谓的"标准"输出,常常是屏幕。除了System.out,Java中还有两个标准流,System.in和System.err。

System.in表示标准输入,它是一个InputStream对象,输入源常常是键盘。好比,从键盘接受一个整数并输出,代码能够为:
```java
Scanner in = new Scanner(System.in);
int num = in.nextInt();
System.out.println(num);
```
System.err表示标准错误流,通常异常和错误信息输出到这个流,它也是一个PrintStream对象,输出目标默认与System.out同样,通常也是屏幕。

标准流的一个重要特色是,它们能够<span style="color:blue">重定向</span>,好比能够重定向到文件,从文件中接受输入,输出也写到文件中。在Java中,可使用System类的setIn, setOut, setErr进行重定向,好比:
```java
System.setIn(new ByteArrayInputStream("hello".getBytes("UTF-8")));
System.setOut(new PrintStream("out.txt"));
System.setErr(new PrintStream("err.txt"));

try{
    Scanner in = new Scanner(System.in);
    System.out.println(in.nextLine());
    System.out.println(in.nextLine());
}catch(Exception e){
    System.err.println(e.getMessage());
}
```
标准输入重定向到了一个ByteArrayInputStream,标准输出和错误重定向到了文件,因此第一次调用in.nextLine就会读取到"hello",输出文件out.txt中也包含该字符串,第二次调用in.nextLine会触发异常,异常消息会写到错误流中,即文件err.txt中会包含异常消息,为"No line found"。

在实际开发中,常常须要重定向标准流。好比,在一些自动化程序中,常常须要重定向标准输入流,以从文件中接受参数,自动执行,避免人手工输入。在后台运行的程序中,通常都须要重定向标准输出和错误流到日志文件,以记录和分析运行的状态和问题。

在Linux系统中,<span style="color:blue">标准输入输出流也是一种重要的协做机制</span>。不少命令都很小,只完成单一功能,实际完成一项工做常常须要组合使用多个命令,它们协做的模式就是经过标准输入输出流,每一个命令均可以从标准输入接受参数,处理结果写到标准输出,这个标准输出能够链接到下一个命令做为标准输入,构成管道式的处理链条。好比,查找一个日志文件access.log中"127.0.0.1"出现的行数,可使用命令:
```
cat access.log | grep 127.0.0.1 | wc -l
```
有三个程序cat, grep, wc,|是管道符号,它将cat的标准输出重定向为了grep的标准输入,而grep的标准输出又成了wc的标准输入。

## 实用方法

能够看出,字符流也包含了不少的类,虽然很灵活,但对于一些简单的需求,却须要写不少代码,实际开发中,常常须要将一些经常使用功能进行封装,提供更为简单的接口。下面咱们提供一些实用方法,以供参考。

### 拷贝

拷贝Reader到Writer,代码为:
```java
public static void copy(final Reader input,
        final Writer output) throws IOException {
    char[] buf = new char[4096];
    int charsRead = 0;
    while ((charsRead = input.read(buf)) != -1) {
        output.write(buf, 0, charsRead);
    }
}
```
### 将文件所有内容读入到一个字符串

参数为文件名和编码类型,代码为:
```java
public static String readFileToString(final String fileName,
        final String encoding) throws IOException{
    BufferedReader reader = null;
    try{
        reader = new BufferedReader(new InputStreamReader(
                new FileInputStream(fileName), encoding));
        StringWriter writer = new StringWriter();
        copy(reader, writer);
        return writer.toString();
    }finally{
        if(reader!=null){
            reader.close();
        }
    }
}
```
这个方法利用了StringWriter,并调用了上面的拷贝方法。

### 将字符串写到文件

参数为文件名、字符串内容和编码类型,代码为:
```java
public static void writeStringToFile(final String fileName,
        final String data, final String encoding) throws IOException {
    Writer writer = null;
    try{
        writer = new OutputStreamWriter(new FileOutputStream(fileName), encoding);
        writer.write(data);
    }finally{
        if(writer!=null){
            writer.close();
        }
    }
}
```
### 按行将多行数据写到文件

参数为文件名、编码类型、行的集合,代码为:
```java
public static void writeLines(final String fileName,
        final String encoding, final Collection<?> lines) throws IOException {
    PrintWriter writer = null;
    try{
        writer = new PrintWriter(fileName, encoding);
        for(Object line : lines){
            writer.println(line);
        }
    }finally{
        if(writer!=null){
            writer.close();
        }
    }
}
```
### 按行将文件内容读到一个列表中

参数为文件名、编码类型,代码为:
```java
public static List<String> readLines(final String fileName,
        final String encoding) throws IOException{
    BufferedReader reader = null;
    try{
        reader = new BufferedReader(new InputStreamReader(
                new FileInputStream(fileName), encoding));
        List<String> list = new ArrayList<>();
        String line = reader.readLine();
        while(line!=null){
            list.add(line);
            line = reader.readLine();
        }
        return list;
    }finally{
        if(reader!=null){
            reader.close();
        }
    }
}
```
Apache有一个类库Commons IO,里面提供了不少简单易用的方法,实际开发中,能够考虑使用。

## 小结

本节咱们介绍了如何在Java中以字符流的方式读写文本文件,咱们强调了二进制思惟、文本文本与二进制文件的区别、编码、以及字符流与字节流的不一样,咱们介绍了个各类字符流、Scanner以及标准流,最后总结了一些实用方法。

写文件时,能够优先考虑PrintWriter,由于它使用方便,支持自动缓冲、支持指定编码类型、支持类型转换等。读文件时,若是须要指定编码类型,须要使用InputStreamReader,不须要,可以使用FileReader,但都应该考虑在外面包上缓冲类BufferedReader。

经过上节和本节,咱们应该能够从容的读写文件内容了,但文件自己的操做,如查看元数据信息、重命名、删除,目录的操做,如遍历文件、查找文件、新建目录等,又该如何进行呢?让咱们下节继续探索。

------
未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),深刻浅出,老马和你一块儿探索Java编程及计算机技术的本质。用心原创,保留全部版权。

![](https://lc-gold-cdn.xitu.io/475ed6bd9976ad39e829.jpg)复制代码
相关文章
相关标签/搜索