這些年一直記不住的 Java I/O

FanCastle 8年前發布 | 19K 次閱讀 Java Java開發

前言

不知道大家看到這個標題會不會笑我,一個使用 Java 多年的老程序員居然一直沒有記住 Java 中的 I/O。不過說實話,Java 中的 I/O 確實含有太多的類、接口和抽象類,而每個類又有好幾種不同的構造函數,而且在 Java 的 I/O 中又廣泛使用了 Decorator 設計模式(裝飾者模式)。總之,即使是在 OO 領域浸淫多年的老手,看到下面這樣的調用一樣會蛋疼:

BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("somefile.txt")));

當然,這僅僅只是我為了體現 Java I/O 的錯綜復雜的構造函數而虛構出來的一個例子,現實中創建一個 BufferedReader 很少會嵌套這么深,因為可以直接使用 FileReader 而避免多創建一個 FileInputStream。但是從一個 InputStream 轉化成一個 BufferedReader 總是有那么幾步路要走的,比如下面這個例子:

URL cnblogs = new URL("http://www.cnblogs.com/");
BufferedReader reader = new BufferedReader(new InputStreamReader(cnblogs.openStream()));

Java I/O 涉及到的類也確實特別多,不僅有分別用于操作字符流和字節流的 InputStream 和 Reader、OutputStream 和 Writer,還有什么 BufferedInputStream、BufferedReader、PrintWriter、PrintStream等,還有用于溝通字節流和字符流的橋梁 InputStreamReader 和 OutputStreamWriter,每一個類都有其不同的應用場景,如此細致的劃分,光是名字就足夠讓人暈頭轉向了。

我一直記不住 Java I/O 中各種細節的另一個原因可能是我深受 ANSI C 的荼毒吧。在 C 語言的標準庫中,將文件的打開方式分為兩種,一種是將文件當成二進制格式打開,一種是當成文本格式打開。這和 Java 中的字節流和字符流的劃分有相似之處,但卻掩蓋了所有的數據其實都是字節流這樣的本質。ANSI C 用多了,總以為二進制格式和文本格式是同一個層面的兩種對立面,只能對立而不能統一,卻不知在 Java 中,字符流是對字節流的更高層次的封裝,最底層的 I/O 都是建立在字節流的基礎上的。如果拋開 ANSI C 語言的標準 I/O 庫,直接考察操作系統層面的 POSIX I/O,會發現操作的一切都是原始的字節數據,根本沒有什么字節字符的區別。

除此之外,Java 走得更遠,它考慮到了各種更加廣泛的字節流,而不僅僅限于文件。比如網絡中傳輸的數據、內存中傳輸的對象等等,都可以用流來抽象。但是不同的流具有不同的特性,有的流可以隨機訪問,而有的卻只能順序訪問,有的可以解釋為字符,有的不能。在能解釋為字符的流中,有的一次只能訪問一個字符,有的卻可以一次訪問一行,而且把字節流解釋成字符流,還要考慮到字符編碼的問題。

以上種種,均是造成 Java I/O 中類和接口多、對象構造方式復雜的原因。

從對立到統一,字節流和字符流

先來說對立。在 Java 中如果要把流中的數據按字節來訪問,就應該使用 InputStream 和 OutputStream,如果要把流中的數據按字符來訪問,就應該使用 Reader 和 Writer。上面提到的這四個類都是抽象類,是所有其它具體類的基礎。不能直接構造 InputStream、OutputStream、Reader 和 Writer 類的實例,但是根據 OO 原則,可以這樣用:

InputStream in = new FileInputStream("somefile");
int c = in.read();

或者這樣:

Reader reader = new FileReader("somefile");
int c = reader.read();

這里的 FileInputStream 和 FileReader 就是具體的類,這樣的類還有很多,都位于 java.io 包中。文件讀寫是我們最常用的操作,所以最常用的就是 FileInputStream、FileOutputStream、FileReader、FileWriter這四個。這幾個類的構造函數有多個,但是最簡單的,肯定是接受一個代表文件路徑的字符串做參數的那一個。根據 OO 原則,我們一般使用更加抽象的 InputStream、OutputStream、Reader、Writer 來引用具體的對象。所以,在考察 API 的時候,只需要考察這四個抽象類就可以了,其它的具體類,基本上只需要考察它們的構造方式。

而這幾個類的 API 也確實很好記,用來輸入的兩個類 InputStream 和 Reader 主要定義了 read() 方法,而用來輸出的兩個類 OutputStream 和 Writer 主要定義了 write() 方法。所不同者,前者操作的是字節,后者操作的是字符。 read() 和 write() 最簡單的用法是這樣的:

package com.xkland.sample;

import java.io.InputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.FileNotFoundException;

public class JavaIODemo { public static void main(String[] args) { if(args.length < 1){ System.out.println("Usage: JavaIODemo filename"); return; } String somefile = args[0]; InputStream in = null; try{ in = new FileInputStream(somefile); int c; while((c = in.read()) != -1) { //這里用到read() System.out.write(c); //這里用到write() } }catch(FileNotFoundException e){ System.out.println("File not found."); }catch(IOException e){ System.out.println("I/O failed."); }finally{ if(in != null){ try { in.close(); }catch(IOException e){ //關閉流時產生的異常,直接拋棄 } } } } }</code></pre>

上面的例子展示了 read() 和 write() 的用法,在 InputStream 和 OutputStream 中,這兩個方法操作的都是字節,但是,這里用來保存這個字節的變量卻是 int 類型的。這正是 API 設計的匠心所在,因為 int 的寬度明顯比 byte 要大,所以將一個 byte 讀入到一個 int 之后,有效的數據只占據 int 型變量的最低8位,如果 read() 方法返回的是有效數據,那么這個 int 型的變量永遠都不可能是負數。在這種情況下, read() 方法可以用返回負數的方式來表示碰到特殊情況,比如返回 -1 表示到達了流的末尾,也就是用 -1 代表 EOF 。 write() 方法接受的參數也是 int 型的,但是它只把這個 int 型變量的最低8位寫入流,其余的數據被忽略。

上面的例子還展示了 Java I/O 的一些特征:

  1. InputStream、OutputStream、Reader、Writer 等資源用完之后要關閉;
  2. 所有的 I/O 操作都可能產生異常,包括調用 close() 方法。

這兩個特征攪到一起就比較復雜了,本來因為異常的產生就容易讓流的 close() 語句執行不到,所以只有把 close() 寫到 finally 塊中,但是在 finally 塊中調用 close() 又要寫一層 try...catch... 代碼塊。如果同時有多個流需要關閉,而前面的 close() 拋出異常,則后面的 close() 將不會執行,極易發生資源泄露。再加上如果前面的 catch() 塊中的異常被重新拋出,而 finally 塊中又沒有處理好異常的話,前面的異常會被抑制,所以大部分人都 hold 不住這樣的代碼,包括 Oracle 的官方教程中的寫法都是錯誤的。下面來看一下 Oracle 官方教程中的例子:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class CopyBytes { public static void main(String[] args) throws IOException {

    FileInputStream in = null;
    FileOutputStream out = null;

    try {
        in = new FileInputStream("xanadu.txt");
        out = new FileOutputStream("outagain.txt");
        int c;

        while ((c = in.read()) != -1) {
            out.write(c);
        }
    } finally {
        if (in != null) {
            in.close();
        }
        if (out != null) {
            out.close();
        }
    }
}

}</code></pre>

官方教程寫得比我更偷懶,它直接讓 main() 方法拋出 IOException 而避免了異常處理,也避免了在 finally 塊中的 close() 語句外再寫一層 try...catch... 。但是,這個示例的漏洞有兩個,其一是如果 in.close() 拋出了異常,則 out.close() 就不會執行;其二是如果 try 塊中拋出了異常, finally 塊中又拋出了異常,則前面拋出的異常會被丟棄。為了解決這個問題,Java 7中新加入了 try-with-resource 語法。后面都用這種方式寫代碼。

很顯然,一次處理一個字節效率是及其低下的,所以 read() 和 write() 還有別的重載版本:

int read(byte[] b)
int read(byte[] b, int off, int len)
void write(byte[] b)
void write(byte[] b, int off, int len)

它們都可以一次操作一塊數據,用字節數組做為存儲數據的容器。 read() 返回的是實際讀取的字節數。而對于 Reader 和 Writer,它們的 read() 和 write() 方法的定義是這樣的:

int read()
int read(char[] cbuf)
int read(char[] cbuf, int off, int len)
void write(int c)
void write(char[] cbuf)
void write(char[] cbuf, int off, int len)
void write(String str)
void write(String str, int off, int len)

可以看出,使用 Reader 和 Writer 一次操作一個字符的時候,依然使用的是 int 型的變量。如果一次操作一塊數據,則使用字符數組。輸出的時候,還可以直接使用字符串。

到這里,已經可以很輕易記住八個類了:InputStream、OutputStream、Reader、Writer、FileInputStream、FileOutputStream、FileReader、FileWriter。前四個是抽象類,后四個是操作文件的具體類。而且這八個類分成兩組,一組操作字節流,一組操作字符流。很簡單的對立分組。

然而,前面我提到過,其實字節流和字符流并不是完全對立的存在,其實字符流是在字節流上更高層次的封裝。在底層,一切數據都是字節,但是經過適當的封裝,可以把這些字節解釋成字符。而且,并不是所有的 Reader 都是可以像 FileReader 那樣直接創建的,有時,只能拿到一個可以讀取字節數據的 InputStream,卻需要在它之上封裝出一個 Reader,以方便按字符的方式讀取數據。最典型的例子就是可以這樣訪問一個網頁:

URL cnblogs = new URL("http://www.cnblogs.com/");
InputStream in = cnblogs.openStream();

這時,拿到的是字節流 InputStream,如果想獲得按字符讀取數據的 Reader,可以這樣創建:

Reader reader = new InputStreamReader(in);

所以, InputStreamReader 是溝通字節流和字符流的橋梁。同樣的橋梁還用用于輸出的 OutputStreamWriter。至此,不僅又輕松地記住了兩個類,也再次證明了字節流和字符流既對立又統一的辯證關系。

從抽象到具體,數據的來源和目的

InputStream、OutputStream、Reader 和 Writer 是抽象的,根據不同的數據來源和目的又有不同的具體類。前面的例子中提到了基于 File 的流,也初步展示了一個基于網絡的流。結合平時使用計算機的經驗,我們也可以想到其它一些不同的數據來源和目的,比如從內存中讀取字節或把字節寫入內存,從字符串中讀取字符或者把字符寫入字符串等等,還有從管道中讀取數據和向管道中寫入數據等等。根據不同的數據來源和目的,可以有這樣一些具體類:FileInputStream、ByteArrayInputStream、PipedInputStream、FileOutputStream、ByteArrayOutputStream、PipedOutputStream、FileReader、StringReader、CharArrayReader、PipedReader、FileWriter、StringWriter、CharArrayWriter、PipedWriter等。從這些類的命名可以看出,凡是以Stream結尾的,都是操作字節的流,凡是以 Reader 和 Writer 結尾的,都是操作字符的流。只有 InputStreamReader 和 OutputStreamWriter 是例外,它是溝通字節和字符的橋梁。對于這些具體類,使用起來是沒有什么困難的,只需要考察它們的構造函數就可以了。下面兩幅 UML 類圖可以展示這些類的關系。

InputStreams 和 Readers:

OutputStreams 和 Writers:

從簡單到豐富,使用 Decorator 模式擴展功能

從前文可以看出,所有的流都支持 read() 和 write() ,但是這樣的功能畢竟還是太簡單,有時還需要更高層次的功能需求,所以需要使用 Decorator 模式來對流進行擴展。比如,一次操作一個字節或一個字符效率太低,想把數據先緩存在內存中再進行操作,就可以擴展出 BufferedInputStream、BufferedReader、BufferedOutputStream、BufferedWriter 類。可以猜測到,BufferedOutputStream 和 BufferedWriter 類中一定有一個 flush() 方法,用來把緩存的數據寫入到流中。而且,BufferedReader 還有 readLine() 方法,可以一次讀取一行字符,甚至可以再擴展出一個 LineNumberReader,還可以提供行號的支持。再比如,有時從流中讀出一個字節或一個字符后,又不想要了,想把它還回去,就可以再擴展出 PushbackInputStream 和 PushbackReader,提供 unread() 方法將剛讀取的字節或字符還回去。可以想象,這種還回去的功能應該是需要緩存功能支持的,所以它們應該是在 BufferedInputStream 和 BufferedReader 外面又加了一層的裝飾。這就是 Decorator 模式。

Java I/O 中自帶的這種擴展類還有很多,不容易記。后面的介紹中,會針對重要的類舉幾個例子。在此之前,還是通過 UML 類圖來了解一下擴展類。

從 InputStream 擴展的類:

從 Reader 擴展的類:

從 OutputStream 擴展的類:

從 Writer 擴展的類:

從上圖中可以看到,每一個分組中擴展的類的數量是不一樣的,再也不是一種對稱的關系。仔細一想也很好理解,例如 Pushback 這樣的功能就只能用在輸入流 InputStream 和 Reader 上,而向輸出流中寫入數據就像潑出去的水,沒辦法再 Pushback 了。再例如,向流中寫入對象和讀取對象,操作的肯定是字節流而不是字符流,所以只有 ObjectInputStream 和 ObjectOutputStream,而沒有相應的 Reader 和 Writer 版本。再例如打印,操作的肯定是輸出流,所以只有 PrintStream 和 PrintWriter,沒有相應的輸入流版本,這沒有什么好奇怪的。

在這些類中,可以通過 PrintStream 和 PrintWriter 向流中寫入格式化的文本,也可以通過 DataInputStream 和 DataOutputStream 從流中讀取或向流中寫入原始的數據,還可以通過 ObjectInputStream 和 ObjectOutputStream 從流中讀取或寫入一個完整的對象。如果要從流中讀取格式化的文本,就必須使用 java.util.Scanner 類了。

下面先看一個簡單的示例,使用 DataOutputStream 的 writeInt() 、 writeDouble() 以及 writeUTF() 方法將 int 、 double 、 String 類型的數據寫入流中,然后再使用 DataInputStream 的 readInt() 、 readDouble() 、 readUTF() 方法從流中讀取 int 、 double 、 String 類型的數據。為了簡單起見,就使用基于文件的流作為存儲數據的方式。代碼如下:

package com.xkland.sample;

import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.EOFException;

public class DataStreamsDemo { public static void writeToFile(String filename){

    double[] prices = { 19.99, 9.99, 15.99, 3.99, 4.99 };
    int[] units = { 12, 8, 13, 29, 50 };
    String[] descs = {
        "Java T-shirt",
        "Java Mug",
        "Duke Juggling Dolls",
        "Java Pin",
        "Java Key Chain"
    };
    try(DataOutputStream out = new DataOutputStream(
            new BufferedOutputStream(
                    new FileOutputStream(filename)))){
        for (int i = 0; i < prices.length; i ++) {
            out.writeDouble(prices[i]);
            out.writeInt(units[i]);
            out.writeUTF(descs[i]);
        }

    }catch(IOException e){
        System.out.println(e.getMessage());
    }
}

public static void readFromFile(String filename){
    double price;
    int unit;
    String desc;
    double total = 0.0;
    try(DataInputStream in = new DataInputStream(
            new BufferedInputStream(
                    new FileInputStream(filename)))){
        while (true) {
            price = in.readDouble();
            unit = in.readInt();
            desc = in.readUTF();
            System.out.format("You ordered %d" + " units of %s at $%.2f%n", unit, desc, price);
            total += unit * price;
        }

    }catch(EOFException e){
        //達到文件末尾
        System.out.format("所有數據已讀入,總價格為:$%.2f%n", total);
    }catch(IOException e){
        System.out.println(e.getMessage());
    }
}

}</code></pre>

然后在 main() 方法中這樣調用:

package com.xkland.sample;

public class JavaIODemo {

public static void main(String[] args) {
    if(args.length < 1){
        System.out.println("Usage: JavaIODemo filename");
        return;
    }
    //向文件中寫入數據
    DataStreamsDemo.writeToFile(args[0]);
    //從文件中讀取數據并顯示
    DataStreamsDemo.readFromFile(args[0]);
}

}</code></pre>

然后這樣運行該程序:

java com.xkland.sample.JavaIODemo /home/youxia/testfile

最后輸出是這樣:

You ordered 12 units of Java T-shirt at $19.99
You ordered 8 units of Java Mug at $9.99
You ordered 13 units of Duke Juggling Dolls at $15.99
You ordered 29 units of Java Pin at $3.99
You ordered 50 units of Java Key Chain at $4.99
所有數據已讀入,總價格為:$892.88

如果使用 cat 命令查看 /home/youxia/testfile 文件的內容,只會看到一堆亂碼,說明該文件是以二進制格式存儲的。如下:

youxia@ubuntu:~$cat testfile

@3?p??

Duke Juggling Dolls@???Q?Java Pin@??(?2Java Key Chain</code></pre>

上面的代碼展示了 DataInputStream 和 DataOutputStream 的用法,通過前面的探討,對它們這樣層層包裝的構造方式已經見怪不怪了。并且在示例代碼中使用了 Java 7 中新引入的 try-with-resource 語法,這樣大大減少了代碼的復雜度,所有打開的流都可以自動關閉,而且異常處理也更簡潔。從代碼中還可以看到,需要捕獲 DataInputStream 的 EOFException 異常才能判斷讀取到了文件結尾。另外,使用這種方式寫入和讀取數據要非常小心,寫入數據的順序和讀取數據的順序一定要保持一致,如果先寫一個 int ,再寫一個 double ,則一定要先讀一個 int ,再讀一個 double ,否則只會讀取錯誤的數據。不信可以通過修改上述示例代碼中讀取數據的順序進行測試。

使用 DataInputStream 和 DataOutputStream 只能寫入和讀取原始的數據類型的數據,如 byte 、 char 、 short 、 float 等,如果要讀取和寫入復雜的對象就不行了,比如 java.math.BigDecimal 。這個時候就需要使用 ObjectInputStream 和 ObjectOutputStream 了。所有需要寫入流和從流讀取的 Object 必須實現 Serializable 接口,然后調用 ObjectInputStream 和 ObjectOutputStream 的 writeObject() 方法和 readObject() 方法就可以了。而且很奇妙的是,如果一個 Object 中包含了其它的 Object 對象,則這些對象都會被寫入到流中,而且能保持它們之間的引用關系。從流中讀取對象的時候,這些對象也會同時被讀入內存,并保持它們之間的引用關系。如果把同一批對象寫入不同的流,再從這些流中讀出,就會獲得這些對象多個副本。這里就不舉例了。

與以二進制格式寫入和讀取數據相對的,就是以文本的方式寫入和讀取數據。PrintStream 和 PrintWriter 中的 Print 就是代表著輸出能供人讀取的數據。比如浮點數 3.14 可以輸出為字符串 "3.14" 。利用 PrintStream 和 PrintWriter 中提供的大量 print() 方法和 println() 方法就可以做到這點,利用 format() 方法還可以進行更加復雜的格式化。把上面的例子做少量修改,如下:

package com.xkland.sample;

import java.io.*; import java.util.Scanner;

public class PrintStreamDemo { public static void writeToFile(String filename){

    double[] prices = { 19.99, 9.99, 15.99, 3.99, 4.99 };
    int[] units = { 12, 8, 13, 29, 50 };
    String[] descs = {
            "Java T-shirt",
            "Java Mug",
            "Duke Juggling Dolls",
            "Java Pin",
            "Java Key Chain"
    };
    try(PrintStream out = new PrintStream(
            new BufferedOutputStream(
                    new FileOutputStream(filename)))){
        for (int i = 0; i < prices.length; i ++) {
            out.println(prices[i]);
            out.println(units[i]);
            out.println(descs[i]);
        }

    }catch(IOException e){
        System.out.println(e.getMessage());
    }
}

public static void readFromFile(String filename){
    double price;
    int unit;
    String desc;
    double total = 0.0;
    try(Scanner s = new Scanner(new BufferedReader(new FileReader(filename)))){
        s.useDelimiter("\n");
        while (s.hasNext()) {
            price = s.nextDouble();
            unit = s.nextInt();
            desc = s.next();
            System.out.format("You ordered %d" + " units of %s at $%.2f%n", unit, desc, price);
            total += unit * price;
        }
        System.out.format("所有數據已讀入,總價格為:$%.2f%n", total);
    }catch(IOException e){
        System.out.println(e.getMessage());
    }
}

}</code></pre>

這時輸出的數據和輸入的數據都是經過良好格式化的,非常便于閱讀和打印,但是在處理數據的時候需要進行適當的轉換和解析,所以會一定程度上影響效率。在使用 java.util.Scanner 時,可以使用 useDelimiter() 方法設置合適的分隔符,在 Linux 系統中,空格、冒號、逗號都是常用的分隔符,具體情況具體分析。在上面的例子中,我直接將每個數據作為一行保存,這樣更加簡單。如果使用 cat 命令查看 /home/youxia/testfile 文件的內容,可以看到格式良好的數據,如下:

youxia@ubuntu:~$ cat testfile
19.99
12
Java T-shirt
9.99
8
Java Mug
15.99
13
Duke Juggling Dolls
3.99
29
Java Pin
4.99
50
Java Key Chain

如果不想使用流,只想像 C 語言那樣簡單地操作文件,可以使用 RandomAccessFile 類。

對于 PrintStream 和 PrintWriter,我們用得最多的就是基于命令行的標準輸入輸出,也就是從鍵盤讀入數據和向屏幕寫入數據。Java 中有幾個內建的對象,它們分別是 System.in、System.out、System.err,因為平時用得多,我就不一一細講了。需要說明的是,這幾個對象都是字節流而不是字符流,這也可以理解,雖然我們的鍵盤不能輸入純二進制數據,但是通過管道和文件重定向卻可以,在控制臺中輸出亂碼也是常見的現象,所以這幾個流必須是字節流而不是字符流。如果要想按字符的方式讀取標準輸入,可以使用 InputStreamReader 這樣轉換一下:

InputStreamReader cin = new InputStreamReader(System.in);

除此之外,還可以使用 System.console 對象,它是 Console 類的一個實例。它提供了幾個實用的方法來操作命令行,如 readLine() 、 readPassword() 等,它的操作是基于字符流的。不過在使用 System.console 之前,先要判斷它是否存在,如果操作系統不支持或程序運行在一個沒有命令行的環境中,則其值為 null 。

Java 7 中引入的 NIO.2

早在 2002 年發布的 Java 1.4 中就引入了所謂的 New I/O,也就是 NIO。但是依然被打臉, NIO 還是不那么好用,還白白浪費了 New 這個詞,搞得 Java 7 中對 I/O 的改進不得不稱為 NIO.2。在 Java 7 之前的 I/O 怎么不好用呢?主要表現在以下幾點:

  1. 在不同的操作系統中,對文件名的處理不一致;
  2. 不方便對目錄樹進行遍歷;
  3. 不能處理符號鏈接;
  4. 沒有一致的文件屬性模型,不能方便地訪問文件的屬性。

所以,雖然存在 java.io.File 類,我前文中卻沒有介紹它。在 Java 7 中,引入了 Path、Paths、Files等類來對文件進行操作。Path 代表文件的路徑,不同操作系統有不同的文件路徑格式,而且還有絕對路徑和相對路徑之分。可以這樣創建路徑:

Path absolute = Paths.get("/", "home", "youxia");
Path relative = Paths.get("myprog", "conf", "user.properties");

靜態方法 Paths.get() 可以接受一個或多個字符串,然后它將這些字符串用文件系統默認的路徑分隔符連接起來。然后它對結果進行解析,如果結果在指定的文件系統上不是一個有效的路徑,那么它會拋出一個 InvalidPathException 異常。當然,也可以給該方法傳遞一個含有分隔符的字符串:

Path home = Paths.get("/home/youxia");

Path 類提供很多有用的方法對路徑進行操作。例如:

Path home = Paths.get("/home/youxia");
Path conf = Paths.get("myprog", "conf", "user.properties");
home.resolve(conf);   // 返回"/home/youxia/myprog/conf/user.properties"
Path another_home = Paths.get("/home/another");
home.relativize(another_home);   //返回相對路徑"../another"
Paths.get("/home/youxia/../another/./myprog").normalize();    //去掉路徑中冗余,返回"/home/another/myprog"
conf.toAbsolutePath();    //根據程序的運行目錄返回絕對路徑,如過在用戶的根目錄中啟動程序,則返回"/home/youxia/myprog/conf/user.properties"
conf.getParent();    //獲得路徑的不含文件名的部分,返回"myprog/conf/"
conf.getFileName();    //獲得文件名,返回"user.properties"
conf.getRoot();    //獲得根目錄

使用 Files 類可以快速實現一些常用的文件操作。例如,可以很容易地讀取一個文件的全部內容:

byte[] bytes = Files.readAllBytes(path);

如果想將文件內容解釋為字符串,可以在 readAllBytes 后調用:

String content = new String(bytes, StandardCharsets.UTF_8);

也可以按行來讀取文件:

List<String> lines = Files.readAllLines(path);

反過來,將一個字符串寫入文件:

Files.write(path, content.getBytes(StandardCharsets.UTF_8));

按行寫入:

Files.write(path, lines);

將內容追加到指定文件中:

Files.write(path, lines, StandardOpenOption.APPEND);

當然,仍然可以使用前文介紹的 InputStream、OutputStream、Reader、Writer 類。這樣創建它們:

InputStream in = Files.newInputStream(path);
OutputStream out = Files.newOutputStream(path);
Reader reader = Files.newBufferedReader(path);
Writer in = Files.newBufferedWriter(path);

同時,使用 Files.copy() 方法,可以簡化某些工作:

Files.copy(in, path);    //將一個 InputStream 中的內容保存到一個文件中
Files.copy(path, out);   //將一個文件的內容復制到一個 OutputStream 中

一些創建、刪除、復制、移動文件和目錄的操作:

Files.createDirectory(path);    //創建一個新目錄
Files.createFile(path);         //創建一個空文件
Files.exists(path);             //檢測一個文件或目錄是否存在
Files.createTempFile(prefix, suffix);  //創建一個臨時文件
Files.copy(fromPath, toPath);   //復制一個文件
Files.move(fromPath, toPath);   //移動一個文件
Files.delete(path);             //刪除一個文件

如果目標文件或目錄存在的話, copy() 和 move() 方法會失敗。如果希望覆蓋一個已存在的文件,可以使用 StandardCopyOption.REPLACE_EXISTING 選項。也可以指定使用原子方式來執行移動操作,這樣要么移動操作成功完成,要么源文件依然存在,可以使用 StandardCopyOption.ATOMIC_MOVE 選項。

可以通過 Files.isSymbolicLink() 方法判斷一個文件是否是符號鏈接,還可以通過 File.readSymbolicLink() 方法讀取該符號鏈接目標的真實路徑。關于文件屬性,Java 7 中提供了 BasicFileAttributes 對真正通用的文件屬性進行了抽象,對于更具體的文件屬性,還提供了 PosixFileAttributes 等類。可以使用 Files.readAttributes() 方法讀取文件的屬性。關于符號鏈接和屬性,來看一個示例:

package com.xkland.sample;

import java.io.IOException; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.PosixFileAttributes;

public class JavaIODemo { public static void main(String[] args) { if(args.length < 1){ System.out.println("Usage: JavaIODemo filename"); return; } Path path = Paths.get(args[0]); Path real = null; try{ if(Files.isSymbolicLink(path)){ real = Files.readSymbolicLink(path); } PosixFileAttributes attr = Files.readAttributes(path, PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS); System.out.format("%s, size: %d, isSymbolicLink: %b .", path, attr.size(), attr.isSymbolicLink()); System.out.println(); PosixFileAttributes attrOfReal = Files.readAttributes(real, PosixFileAttributes.class); System.out.format("%s, size: %d, isSymbolicLink: %b .", real, attrOfReal.size(), attrOfReal.isSymbolicLink()); System.out.println(); } catch (IOException e) { e.printStackTrace(); } } }</code></pre>

如果這樣運行程序,可以查看 /etc/alternatives/js 文件是否是符號鏈接,并查看具體鏈接到哪個文件:

youxia@ubuntu:~$java com.xkland.sample.JavaIODemo /etc/alternatives/js
/etc/alternatives/js, size: 15, isSymbolicLink: true .
/usr/bin/nodejs, size: 11187096, isSymbolicLink: false .

NIO.2 API 會默認跟隨符號鏈接,如果不要上述示例代碼中的 LinkOption.NOFOLLOW_LINKS 選項,則 Files.readAttributes() 返回的結果就是實際文件的屬性,而不是符號鏈接文件的屬性。

NIO.2 中的異步 I/O

由于 I/O 操作經常會阻塞,所以編寫異步 I/O 操作的代碼從來都是提高程序運行效率的有效手段。特別是 Node.js 的出現,使異步 I/O 的影響達到空前的巨大,基于 Callback 的異步 I/O 早已深入人心。 Java 7 中有三個新的異步通道:

  1. AsynchronousFileChannel —— 用于文件 I/O;
  2. AsynchronousSocketChannel —— 用于套接字 I/O,支持超時;
  3. AsynchronousServerSocketChannel —— 用于套接字接受異步鏈接。

這里只考察一下基于文件的異步 I/O。使用異步 I/O 有兩種形式,一種是基于 Future,一種是基于 Callback。使用 Future 的示例代碼如下:

try{
    Path file = Paths.get("/home/youxia/testfile");
    AsynchronousFileChannel channel = AsynchronousFileChannel.open(file);    //異步打開文件
    ByteBuffer buffer = ByteBuffer.allocate(100_000);
    Future<Integer> result = channel.read(buffer, 0);    //讀取 100 000 字節
    while(!result.isDone()){
        //干點兒別的事情
    }
    Integer bytesRead = result.get();    //獲取結果
    System.out.println("已讀取的字節數:" + bytesRead);
}catch(IOException | ExecutionException | InterruptedException e){
    System.out.println(e.getMessage());
}

如果使用基于 Callback 的異步 I/O,其示例代碼是這樣的:

try{
    Path file = Paths.get("/home/youxia/testfile");
    AsynchronousFileChannel channel = AsynchronousFileChannel.open(file);
    ByteBuffer buffer = ByteBuffer.allocate(100_000);  //異步方式打開文件,分配緩沖區準備讀取,和前面是一樣的

channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>(){

    public void completed(Integer result, ByteBuffer attachment){
        System.out.println("已讀取的字節數:" + bytesRead);
    }

    public void failed(Throwable exception, ByteBuffer attachment){
        System.out.println(exception.getMessage());
    }
});  //調用 channel.read() 的另一個版本,接受一個 CompletionHandler 類的對象做參數

}catch(IOException e){ System.out.println(e.getMessage()); }</code></pre>

在這里,創建了一個回調對象,該對象有 completed() 方法和 failed() 方法,根據 I/O 操作是否成功相應的方法會被回調,這和 Node.js 中的異步 I/O 是何其的相似啊。

總結

寫完這一篇,估計我是再也不會忘記 Java I/O 的用法了。認真讀完我這一篇的朋友應該也一樣,如果讀一遍又忘記了的話,就多讀幾遍。當然,我這一篇文章仍不可能包含 Java I/O 的方方面面。關于具體的 API,大家直接查看 Oracle 的官方文檔就可以了。

 

來自:http://www.cnblogs.com/youxia/p/java007.html

 

 本文由用戶 FanCastle 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!