1. 程式人生 > >34 JAVA程式設計思想——JAVA IO 流典型應用

34 JAVA程式設計思想——JAVA IO 流典型應用

               

34.JAVA程式設計思想——JAVA IO 流典型應用

儘管庫記憶體在大量IO 流類,可通過多種不同的方式組合到一起,但實際上只有幾種方式才會經常用到。然而,必須小心在意才能得到正確的組合。下面這個相當長的例子展示了典型IO 配置的建立與使用,可在寫自己的程式碼時將其作為一個參考使用。注意每個配置都以一個註釋形式的編號起頭,並提供了適當的解釋資訊。

輸入流

經常想做的一件事情是將格式化的輸出列印到控制檯

1.      緩衝的輸入檔案

為開啟一個檔案以便輸入,需要使用一個FileInputStream,同時將一個 String或 File 物件作為檔名使用。為提高速度,最好先對檔案進行緩衝處理,從而獲得用於一個BufferedInputStream 的構建器的結果控制代碼。為了以格式化的形式讀取輸入資料,我們將那個結果控制代碼賦給用於一個DataInputStream 的構建器。

DataInputStream是我們的最終(final)物件,並是我們進行讀取操作的介面。

在這個例子中,只用到了readLine()方法,但理所當然任何 DataInputStream方法都可以採用。一旦抵達檔案末尾,readLine()就會返回一個null(空),以便中止並退出while 迴圈。

“String s2”用於聚集完整的檔案內容(包括必須新增的新行,因為 readLine()去除了那些行)。隨後,在後面部分中使用 s2。最後,呼叫 close(),用它關閉檔案。從技術上說,會在執行finalize()時呼叫close()。而且我們希望一旦程式退出,就發生這種情況(無論是否進行垃圾收集)。然而,Java 1.0 有一個非常突出的錯誤(Bug),造成這種情況不會發生。在Java 1.1 中,必須明確呼叫System.runFinalizersOnExit(true),用它保證會為系統中的每個物件呼叫finalize()。然而,最安全的方法還是為檔案明確呼叫 close()。

2.      從記憶體輸入

這一部分採用已經包含了完整檔案內容的String s2,並用它建立一個 StringBufferInputStream(字串緩衝輸入流)——作為構建器的引數,要求使用一個 String,而非一個 StringBuffer)。隨後,我們用 read()依次讀取每個字元,並將其傳送至控制檯。注意read()將下一個位元組返回為int,所以必須將其造型為一個char,以便正確地列印。

3.      格式化記憶體輸入

StringBufferInputStream 的介面是有限的,所以通常需要將其封裝到一個DataInputStream 內,從而增強它的能力。然而,若選擇用readByte()每次讀出一個字元,那麼所有值都是有效的,所以不可再用返回值來偵測何時結束輸入。相反,可用available()方法判斷有多少字元可用。下面這個例子展示瞭如何從檔案中一次讀出一個字元:

程式碼

import java.io.*;

publicclassTestEOF {

    publicstatic void main(String[]args) {

        try {

            DataInputStreamin=newDataInputStream(newBufferedInputStream(new FileInputStream("TestEof.java")));

            while (in.available() != 0)

                System.out.print((char)in.readByte());

        }catch(IOExceptione) {

            System.err.println("IOException");

        }

    }

} /// :~

說明

注意取決於當前從什麼媒體讀入,avaiable()的工作方式也是有所區別的。它在字面上意味著“可以不受阻塞讀取的位元組數量”。對一個檔案來說,它意味著整個檔案。但對一個不同種類的資料流來說,它卻可能有不同的含義。因此在使用時應考慮周全。

為了在這樣的情況下偵測輸入的結束,也可以通過捕獲一個違例來實現。然而,若真的用違例來控制資料流,卻顯得有些大材小用。

4.      行的編號與檔案輸出

這個例子展示瞭如何LineNumberInputStream來跟蹤輸入行的編號。在這裡,不可簡單地將所有構建器都組合起來,因為必須保持 LineNumberInputStream的一個控制代碼(注意這並非一種繼承環境,所以不能簡單地將in4造型到一個 LineNumberInputStream)。因此,li 容納了指向 LineNumberInputStream的控制代碼,然後在它的基礎上建立一個DataInputStream,以便讀入資料。

這個例子也展示瞭如何將格式化資料寫入一個檔案。首先建立了一個FileOutputStream,用它同一個檔案連線。考慮到效率方面的原因,它生成了一個BufferedOutputStream。這幾乎肯定是我們一般的做法,但卻必須明確地這樣做。隨後為了進行格式化,它轉換成一個PrintStream。用這種方式建立的資料檔案可作為一個原始的文字檔案讀取。

標誌DataInputStream何時結束的一個方法是 readLine()。一旦沒有更多的字串可以讀取,它就會返回null。每個行都會伴隨自己的行號列印到檔案裡。該行號可通過li 查詢。

可看到用於 out1 的、一個明確指定的close()。若程式準備掉轉頭來,並再次讀取相同的檔案,這種做法就顯得相當有用。然而,該程式直到結束也沒有檢查檔案IODemo.txt。正如以前指出的那樣,如果不為自己的所有輸出檔案呼叫close(),就可能發現緩衝區不會得到重新整理,造成它們不完整。

輸出流

兩類主要的輸出流是按它們寫入資料的方式劃分的:一種按人的習慣寫入,另一種為了以後由一個DataInputStream而寫入。RandomAccessFile是獨立的,儘管它的資料格式兼容於 DataInputStream和DataOutputStream。

5.      儲存與恢復資料

PrintStream能格式化資料,使其能按我們的習慣閱讀。但為了輸出資料,以便由另一個數據流恢復,則需用一個DataOutputStream 寫入資料,並用一個DataInputStream 恢復(獲取)資料。當然,這些資料流可以是任何東西,但這裡採用的是一個檔案,並進行了緩衝處理,以加快讀寫速度。

注意字串是用writeBytes()寫入的,而非writeChars()。若使用後者,寫入的就是16 位Unicode 字元。由於DataInputStream 中沒有補充的“readChars”方法,所以不得不用 readChar()每次取出一個字元。所以對ASCII 來說,更方便的做法是將字元作為位元組寫入,在後面跟隨一個新行;然後再用readLine()將字元當作普通的ASCII 行讀回。

writeDouble()將double 數字儲存到資料流中,並用補充的 readDouble()恢復它。但為了保證任何讀方法能夠正常工作,必須知道資料項在流中的準確位置,因為既有可能將儲存的 double資料作為一個簡單的位元組序列讀入,也有可能作為 char 或其他格式讀入。所以必須要麼為檔案中的資料採用固定的格式,要麼將額外的資訊儲存到檔案中,以便正確判斷資料的存放位置。

6.      讀寫隨機訪問檔案

正如早先指出的那樣,RandomAccessFile 與 IO層次結構的剩餘部分幾乎是完全隔離的,儘管它也實現了DataInput 和DataOutput 介面。所以不可將其與 InputStream及 OutputStream子類的任何部分關聯起來。

儘管也許能將一個ByteArrayInputStream 當作一個隨機訪問元素對待,但只能用RandomAccessFile開啟一個檔案。必須假定RandomAccessFile 已得到了正確的緩衝,因為我們不能自行選擇。

可以自行選擇的是第二個構建器引數:可決定以“只讀”(r)方式或“讀寫”(rw)方式開啟一個RandomAccessFile 檔案。

使用RandomAccessFile的時候,類似於組合使用 DataInputStream和 DataOutputStream(因為它實現了等同的介面)。除此以外,還可看到程式中使用了seek(),以便在檔案中到處移動,對某個值作出修改。

快捷檔案處理

由於以前採用的一些典型形式都涉及到檔案處理,所以大家也許會懷疑為什麼要進行那麼多的程式碼輸入——這正是裝飾器方案一個缺點。本部分將向大家展示如何建立和使用典型檔案讀取和寫入配置的快捷版本。這些快捷版本均置入packagecom.bruceeckel.tools 中。為了將每個類都新增到庫內,只需將其置入適當的目錄,並新增對應的package 語句即可。

7.      快速檔案輸入

若想建立一個物件,用它從一個緩衝的DataInputStream中讀取一個檔案,可將這個過程封裝到一個名為InFile的類內。如下

程式碼

import java.io.*;

publicclassInFile extendsDataInputStream {

    public InFile(Stringfilename) throws FileNotFoundException {

        super(new BufferedInputStream(new FileInputStream(filename)));

    }

    public InFile(Filefile) throws FileNotFoundException {

        this(file.getPath());

    }

} /// :~

無論構建器的String 版本還是File 版本都包括在內,用於共同建立一個 FileInputStream。

就象這個例子展示的那樣,現在可以有效減少建立檔案時由於重複強調造成的問題。

8.      快速輸出格式化檔案

亦可用同類型的方法建立一個PrintStream,令其寫入一個緩衝檔案。下面是對com.bruceeckel.tools 的擴充套件:

程式碼2

import java.io.*;

publicclassPrintFile extendsPrintStream {

    public PrintFile(Stringfilename) throws IOException {

        super(new BufferedOutputStream(new FileOutputStream(filename)));

    }

    public PrintFile(Filefile) throws IOException {

        this(file.getPath());

    }

} /// :~

注意構建器不可能捕獲一個由基礎類構建器“擲”出的違例。

9.      快速輸出資料檔案

最後,利用類似的快捷方式可建立一個緩衝輸出檔案,用它儲存資料(與由人觀看的資料格式相反):

程式碼3

import java.io.*;

publicclassOutFile extendsDataOutputStream {

    public OutFile(Stringfilename) throws IOException {

        super(new BufferedOutputStream(new FileOutputStream(filename)));

    }

    public OutFile(Filefile) throws IOException {

        this(file.getPath());

    }

} /// :~

非常奇怪的是(也非常不幸),Java 庫的設計者居然沒想到將這些便利措施直接作為他們的一部分標準提供。

從標準輸入中讀取資料

以Unix 首先倡導的“標準輸入”、“標準輸出”以及“標準錯誤輸出”概念為基礎,Java 提供了相應的System.in,System.out 以及System.err。貫這一整本書,大家都會接觸到如何用 System.out進行標準輸出,它已預封裝成一個 PrintStream物件。System.err 同樣是一個PrintStream,但System.in是一個原始的InputStream,未進行任何封裝處理。這意味著儘管能直接使用 System.out 和System.err,但必須事先封裝System.in,否則不能從中讀取資料。

典型情況下,我們希望用readLine()每次讀取一行輸入資訊,所以需要將System.in 封裝到一個DataInputStream中。這是 Java 1.0進行行輸入時採取的“老”辦法。下面是個簡單的例子,作用是迴應我們鍵入的每一行內容:

程式碼4

import java.io.*;

publicclassEcho {

    publicstatic void main(String[]args) {

        DataInputStreamin =new DataInputStream(newBufferedInputStream(System.in));

        Strings;

        try {

            while ((s =in.readLine()).length()!= 0)

                System.out.println(s);

            // An empty line terminates the program

        }catch(IOExceptione) {

            e.printStackTrace();

        }

    }

} /// :~

之所以要使用try塊,是由於 readLine()可能“擲”出一個 IOException。注意同其他大多數流一樣,也應對System.in 進行緩衝。

由於在每個程式中都要將System.in 封裝到一個 DataInputStream 內,所以顯得有點不方便。但採用這種設計方案,可以獲得最大的靈活性。

管道資料流

PipedInputStream(管道輸入流)和PipedOutputStream(管道輸出流)。儘管描述不十分詳細,但並不是說它們作用不大。然而,只有在掌握了多執行緒處理的概念後,才可真正體會它們的價值所在。原因很簡單,因為管道化的資料流就是用於執行緒之間的通訊。