計算機程序的思惟邏輯 (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)複製代碼
相關文章
相關標籤/搜索