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

本文目錄html

  • 參考資料java

  • 前言程序員

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

  • 從抽象到具體,數據的來源和目的設計模式

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

  • Java 7 中引入的 NIO.2緩存

  • NIO.2 中的異步 I/O微信

  • 總結網絡

參考資料

  該文中的內容來源於 Oracle 的官方文檔。Oracle 在 Java 方面的文檔是很是完善的。對 Java 8 感興趣的朋友,能夠從這個總入口Java SE 8 Documentation開始尋找感興趣的內容。這一篇主要講 Java 中的 I/O,官方文檔在這裏Java I/O, NIO, and NIO.2oracle

前言

  不知道你們看到這個標題會不會笑我,一個使用 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){
                   //關閉流時產生的異常,直接拋棄
               }
           }
       }
   }
}

  上面的例子展現了read()write()的用法,在 InputStream 和 OutputStream 中,這兩個方法操做的都是字節,可是,這裏用來保存這個字節的變量倒是int類型的。這正是 API 設計的匠心所在,由於int的寬度明顯比byte要大,因此將一個byte讀入到一個int以後,有效的數據只佔據int型變量的最低8位,若是read()方法返回的是有效數據,那麼這個int型的變量永遠都不多是負數。在這種狀況下,read()方法能夠用返回負數的方式來表示碰到特殊狀況,好比返回-1表示到達了流的末尾,也就是用-1表明EOFwrite()方法接受的參數也是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();
           }
       }
   }
}

  官方教程寫得比我更偷懶,它直接讓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()方法將intdoubleString類型的數據寫入流中,而後再使用 DataInputStream 的readInt()readDouble()readUTF()方法從流中讀取intdoubleString類型的數據。爲了簡單起見,就使用基於文件的流做爲存儲數據的方式。代碼以下:

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());
       }
   }
}

  而後在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]);
   }

}

  而後這樣運行該程序:

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文件的內容,只會看到一堆亂碼,說明該文件是以二進制格式存儲的。以下:

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

  使用 DataInputStream 和 DataOutputStream 只能寫入和讀取原始的數據類型的數據,如bytecharshortfloat等,若是要讀取和寫入複雜的對象就不行了,好比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());
       }
   }
}

  這時輸出的數據和輸入的數據都是通過良好格式化的,很是便於閱讀和打印,可是在處理數據的時候須要進行適當的轉換和解析,因此會必定程度上影響效率。在使用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();
       }
   }
}

  若是這樣運行程序,能夠查看/etc/alternatives/js文件是不是符號連接,並查看具體連接到哪一個文件:

youxia@ubuntu:~$java com.xkland.sample.JavaIODemo /etc/alternatives/java
/etc/alternatives/java, size: 35, isSymbolicLink: true .
/usr/java/jdk1.8.0_102/jre/bin/java, size: 7734, 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());
}

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

總結

  寫完這一篇,估計我是不再會忘記 Java I/O 的用法了。認真讀完我這一篇的朋友應該也同樣,若是讀一遍又忘記了的話,就多讀幾遍。固然,我這一篇文章仍不可能包含 Java I/O 的方方面面。關於具體的 API,你們直接查看 Oracle 的官方文檔就能夠了。讀到這裏的朋友,請不要忘記給個贊,謝謝。

我有一個微信公衆號,常常會分享一些Java技術相關的乾貨;若是你喜歡個人分享,能夠用微信搜索「Java團長」或者「javatuanzhang」關注。

相關文章
相關標籤/搜索