程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> TIJ閱讀筆記(第十二章)

TIJ閱讀筆記(第十二章)

編輯:關於JAVA

12: Java I/O 系統

對編程語言的設計者來說,創建一套好的輸入輸出(I/O)系統,是一項難度極高的任務。

File 類

在介紹直接從流裡讀寫數據的類之前,我們先介紹一下處理文件和目錄的類。

你會認為這是一個關於文件的類,但它不是。你可以用它來表示某個文件的名字,也可以用它來表示目錄裡一組文件的名字。如果它表示的是一組文件,那麼你還可以用list( )方法來進行查詢,讓它會返回String數組。由於元素數量是固定的,因此數組會比容器更好一些。如果你想要獲取另一個目錄的清單,再建一個File對象就是了。

目錄列表器

假設你想看看這個目錄。有兩個辦法。一是不帶參數調用list( )。它返回的是File對象所含內容的完整清單。但是,如果你要的是一個"限制性列表(restricted list)"的話 —— 比方說,你想看看所有擴展名為.Java的文件 —— 那麼你就得使用"目錄過濾器"了。這是一個專門負責挑選顯示File對象的內容的類。

FilenameFilter接口的聲明:

public interface FilenameFilter { boolean accept(File dir, String name);}

accept( )方法需要兩個參數,一個是File對象,表示這個文件是在哪個目錄裡面的;另一個是String,表示文件名。雖然你可以忽略它們中的一個,甚至兩個都不管,但是你大概總得用一下文件名吧。記住,list( )會對目錄裡的每個文件調用accept( ),並以此判斷是不是把它包括到返回值裡;這個判斷依據就是accept( )的返回值。

切記,文件名裡不能有路徑信息。為此你只要用一個String對象來創建File對象,然後再調用這個File對象的getName( )就可以了。它會幫你剝離路徑信息(以一種平台無關的方式)。然後再在accept( )裡面用正則表達式(regular expression)的matcher對象判斷,regex是否與文件名相匹配。兜完這個圈子,list( )方法返回了一個數組。

匿名內部類

注意,filter( )的參數必須是final的。要想在匿名內部類裡使用其作用域之外的對象,只能這麼做。

可以用匿名內部類來創建專門供特定問題用的,一次性的類。這種做法的好處是,它能把解決某個問題的代碼全都集中到一個地方。但是從另一角度來說,這樣做會使代碼的可讀性變差,所以要慎重。

查看與創建目錄

File類的功能不僅限於顯示文件或目錄。它還能幫你創建新的目錄甚至是目錄路徑(directory path),如果目錄不存在的話。此外它還能用來檢查文件的屬性(大小,上次修改的日期,讀寫權限等),判斷File對象表示的是文件還是目錄,以及刪除文件。

renameTo( )這個方法會把文件重命名成(或者說移動到)新的目錄,也就是參數所給出的目錄。而參數本身就是一個File對象。這個方法也適用於目錄。

輸入與輸出

I/O類庫常使用"流(stream)"這種抽象。所謂"流"是一種能生成或接受數據的,代表數據的源和目標的對象。流把I/O設備內部的具體操作給隱藏起來了。

Java的I/O類庫分成輸入和輸出兩大部分。所有InputStream和Reader的派生類都有一個基本的,繼承下來的,能讀取單個或byte數組的read( )方法。同理,所有OutputStream和Writer的派生類都有一個基本的,能寫入單個或byte數組的write( )方法。但通常情況下,你是不會去用這些方法的;它們是給其它類用的 —— 而後者會提供一些更實用的接口。因此,你很少會碰到只用一個類就能創建一個流的情形,實際上你得把多個對象疊起來,並以此來獲取所需的功能。Java的流類庫之所以會那麼讓人犯暈,最主要的原因就是"你必須為創建一個流而動用多個對象"。

InputStream的種類

InputStream的任務就是代表那些能從各種輸入源獲取數據的類。這些源包括:

byte數組String對象文件類似流水線的"管道(pipe)"。把東西從一頭放進去,讓它從另一頭出來。一個"流的序列(A sequence of other streams)",可以將它們組裝成一個單獨的流。其它源,比如Internet的連接。(這部分內容在Thinking in Enterprise Java中討論。)

這些數據源各自都有與之相對應的InputStream的子類。此外,FilterInputStream也是InputStream的子類,其作用是為基類提供"decorator(修飾)"類,而decorator又是為InputStream配置屬性和接口的。

表12-1. InputStream的種類類功能構造函數的參數用法ByteArrayInputStream以緩沖區內存為InputStream要從中提取byte的那個緩沖區一種數據源:要把它連到FilterInputStream對象,由後者提供接口。StringBufferInputStream以String為InputStream需要一個String對象。實際上程序內部用的是StringBuffer。一種數據源:要把它連到FilterInputStream對象,由後者提供接口。FileInputStream專門用來讀文件的一個表示文件名的String對象,也可以是File或 FileDescriptor對象。一種數據源:要把它連到FilterInputStream對象,由後者提供接口。PipedInputStream從PipedOutputStream提取數據。實現"管道"功能。PipedOutputStream一種多線程環境下的數據源,把它連到FilterInputStream對象,由後者提供的接口。SequenceInputStream將兩個或更多的InputStream合並成一個InputStream。兩個InputStream對象,或一個InputSteam對象容器的Enumerator一種數據源:要把它連到FilterInputStream對象,由後者提供接口。FilterInputStream一個為decorator定義接口用的抽象類。而decorator的作用是為InputStream實現具體的功能。詳見表12-3。見表 12-3見表 12-3

OutputStream的種類

這部分都是些決定往哪裡輸出的類:是byte的數組(不能是String;不過你可以根據byte數組創建字符串)還是文件,或者是"管道"。

此外,FilterOutputStream還是decorator類的基類。它會為OutputStream安裝屬性和適用的接口。

表12-2. OutputStream的種類類功能構造函數的參數用法ByteArrayOutputStream在內存裡創建一個緩沖區。數據送到流裡就是寫入這個緩沖區。緩沖區初始大小,可選。要想為數據指定目標,可以用FilterOutputStream對其進行包裝,並提供接口。FileOutputStream將數據寫入文件。一個表示文件名的字符串,也可以是File或FileDescriptor對象。要想為數據指定目標,可以用FilterOutputStream對其進行包裝,並提供接口。PipedOutputStream寫入這個流的數據,最終都會成為與之相關聯的PipedInputStream的數據源。否則就不成其為"管道"了。PipedInputStream要想在多線程環境下為數據指定目標,可以用FilterOutputStream對其進行包裝,並提供接口。FilterOutputStream一個給decorator提供接口用的抽象類。而decorator的作用是為OutputStream實現具體的功能。詳見表12-4見表12-4見表12-4

添加屬性與適用的接口

使用"分層對象(layered objects)",為單個對象動態地,透明地添加功能的做法,被稱為Decorator Pattern。(模式是Thinking in Patterns (with Java)的主題。)Decorator模式要求所有包覆在原始對象之外的對象,都必須具有與之完全相同的接口。這使得decorator的用法變得非常的透明--無論對象是否被decorate過,傳給它的消息總是相同的。這也是Java I/O類庫要有"filter(過濾器)"類的原因:抽象的"filter"類是所有decorator的基類。(decorator必須具有與它要包裝的對象的全部接口,但是decorator可以擴展這個接口,由此就衍生出了很多"filter"類)。

Decorator模式常用於如下的情形:如果用繼承來解決各種需求的話,類的數量會多到不切實際的地步。Java的I/O類庫需要提供很多功能的組合,於是decorator模式就有了用武之地。但是decorator有個缺點,在提高編程的靈活性的同時(因為你能很容易地混合和匹配屬性),也使代碼變得更復雜了。Java的I/O類庫之所以會這麼怪,就是因為它"必須為一個I/O對象創建很多類",也就是為一個"核心"I/O類加上很多decorator。

為InputStream和OutputStream定義decorator類接口的類,分別是FilterInputStream和FilterOutputStream。這兩個名字都起得不怎麼樣。FilterInputStream和FilterOutputStream都繼承自I/O類庫的基類InputStream和OutputStream,這是decorator模式的關鍵(惟有這樣decorator類的接口才能與它要服務的對象的完全相同)。

用FilterInputStream讀取InputStream

FilterInputStream及其派生類有兩項重要任務。DataInputStream可以讀取各種primitive及String。(所有的方法都以"read"打頭,比如readByte( ), readFloat( ))。它,以及它的搭檔DataOutputStream,能讓你通過流將primitive數據從一個地方導到另一個地方。這些"地方"都列在表12-1裡。

其它的類都是用來修改InputStream的內部行為的:是不是做緩沖,是不是知道它所讀取的行信息(允許你讀取行號或設定行號),是不是會彈出單個字符。後兩個看上去更像是給編譯器用的(也就是說,它們大概是為Java編譯器設計的),所以通常情況下,你是不大會用到它們的。

不論你用哪種I/O設備,輸入的時候,最好都做緩沖。所以對I/O類庫來說,比較明智的做法還是把不緩沖當特例(或者去直接調用方法),而不是像現在這樣把緩沖當作特例。

表12-3. FilterInputStream的種類類功能構造函數的參數用法DataInputStream與DataOutputStream配合使用,這樣你就能以一種"可攜帶的方式(portable fashion)"從流裡讀取primitives了(int,char,long等)InputStream包含了一整套讀取primitive數據的接口。BufferedInputStream用這個類來解決"每次要用數據的時候都要進行物理讀取"的問題。你的意思是"用緩沖區。"InputStream,以及可選的緩沖區的容量它本身並不提供接口,只是提供一個緩沖區。需要連到一個"有接口的對象(interface object)"。LineNumberInputStream跟蹤輸入流的行號;有getLineNumber( )和setLineNumber(int)方法InputStream只是加一個行號,所以還得連一個"有接口的對象"。PushbackInputStream有一個"彈壓單字節"的緩沖區(has a one byte push-back buffer),這樣你就能把最後讀到的那個字節再壓回去了。InputStream主要用於編譯器的掃描程序。可能是為支持Java的編譯器而設計的。用的機會不多。

用FilterOutputStream往OutputStream裡面寫東西

DataInputStream的另一半是DataOutputStream。它的任務是把primitive數據和String對象重新組織成流,這樣其它機器就能用DataInputStream讀取這個流了。DataOutputStream的方法都是以"write"開頭的,比如writeByte( ),writeFloat( )等等。

PrintStream的用意是要以一種大家都能看懂的方式把primitive數據和String對象打印出來。這一點同DataOutputStream不同,後者是要將數據裝入一個流,然後再交給 DataInputStream處理。

PrintStream的兩個最重要的方法是print( )和println( )。這兩個方法都已經作了重載,因此可以打印各種數據。print( )和println( )的區別在於,後者會多打印一個換行符。

使用PrintStream的時候會比較麻煩,因為它會捕捉所有的IOException(所以你必須直接調用checkError( )來檢查錯誤條件,因為這個方法會在碰到問題的時候返回true)。再加上,PrintStream的國際化做得也不好,而且還不能以與平台無關的方式處理換行(這些問題都已經在PrintWriter裡得到解決,我們接下來再講)。

BufferedOutputStream 是個decorator,它表示對流作緩沖,這樣每次往流裡寫東西的時候它就不會再每次都作物理操作了。輸出的時候大致都要這麼做。

表12-4. FilterOutputStream的種類類功能構造函數的參數用法DataOutputStream與DataInputStream配合使用,這樣你就可以用一種"可攜帶的方式(portable fashion)"往流裡寫primitive了(int, char, long,等)OutputStream包括寫入primitive數據的全套接口。PrintStream負責生成帶格式的輸出(formatted output)。DataOutputStrem負責數據的存儲,而PrintStream負責數據的顯示。一個OutputStream以及一個可選的boolean值。這個boolean值表示,要不要清空換行符後面的緩沖區。應該是OutputStream對象的最終包覆層。用的機會很多。BufferedOutputStream用 這個類解決"每次往流裡寫數據,都要進行物理操作"的問題。也就是說"用緩沖區"。用flush( )清空緩沖區。OutputStream, 以及一個可選的緩沖區大小本身並不提供接口,只是加了一個緩沖區。需要鏈接一個有接口的對象。

Reader 和 Writer類系

Java 1.1對最底層的I/O流類庫作了重大修改。第一次看到Reader和Writer的時候,你會覺得"它們大概是用來取代InputStream和OutputStream的" (和我一樣)。但事實並非如此。雖然InputStream和OutputStream的某些功能已經淘汰了(如果你繼續使用,編譯器就會發警告),但它們仍然提供了很多很有價值的,面向byte的I/O功能,而Reader和Writer則提供了Unicode兼容的,面向字符的I/O功能。此外:

Java 1.1還對InputStream和OutputStream作了新的補充,所以很明顯這兩個類系並沒有被完全替代。有時,你還必須同時使用"基於byte的類"和"基於字符的類"。為此,它還提供了兩個"適配器(adapter)"類。InputStreamReader負責將InputStream轉化成Reader,而OutputStreamWriter則將OutputStream轉化成Writer。

Reader和Writer要解決的,最主要的問題就是國際化。原先的I/O類庫只支持8位的字節流,因此不可能很好地處理16位的Unicode字符流。Unicode是國際化的字符集(更何況Java內置的char就是16位的Unicode字符),這樣加了Reader和Writer之後,所有的I/O就都支持Unicode了。此外新類庫的性能也比舊的好。

數據源和目的

幾乎所有的Java I/O流都有與之對應的,專門用來處理Unicode的Reader和Writer。但有時,面向byte的InputStream和OutputStream才是正確的選擇;特別是Java.util.zip;它的類都是面向byte的。所以最明智的做法是,先用Reader和Writer,等到必須要用面向byte的類庫時,你自然會知道的,因為程序編譯不過去了。

下面這張表格列出了這兩個類系的數據源和目的之間的關系(也就是說,在這兩個類系裡,數據是從哪裡來的,又是到那裡去的)。

數據源和目的Java 1.0的類Java 1.1的類InputStreamReader的適配器:InputStreamReaderOutputStreamWriter的適配器: OutputStreamWriterFileInputStreamFileReaderFileOutputStreamFileWriterStringBufferInputStreamStringReader(沒有對應的類)StringWriterByteArrayInputStreamCharArrayReaderByteArrayOutputStreamCharArrayWriterPipedInputStreamPipedReaderPipedOutputStreamPipedWriter

總之,這兩個類系即便不是一摸一樣,也至少是非常相像。

修改流的行為

不管是InputStream還是OutputStream,用的時候都要先交給FilterInputStream和FilterOutputStrem,並由後者,也就是decorator做一番改造。Reader和Writer繼承了這一傳統,不過不是完全照搬。

下面這張表的對應關系比前面那張更粗略。這是因為這兩個類系的組織結構不同。比方說BufferedOutputStream是FilterOutputStream的子類,但BufferedWriter卻不是FilterWriter的子類(後者雖然是一個abstract類,但卻沒有子類,所以它看上去只是起一個"占位子"的作用,這樣你就不會去惦記它在哪裡了)。但不管怎麼說,它們的接口還是很相似的。

Filter類Java 1.0的類Java 1.1的類FilterInputStreamFilterReaderFilterOutputStreamFilterWriter(這是個無派生類的抽象類)BufferedInputStreamBufferedReader(也有readLine( ))BufferedOutputStreamBufferedWriterDataInputStream盡量用DataInputStream(除非你用BufferedReader的時候要用readLine( ))PrintStreamPrintWriterLineNumberInputStream(過時了)LineNumberReaderStreamTokenizerStreamTokenizer(換一個構造函數,把Reader當參數傳給它)PushBackInputStreamPushBackReader

有一條很清楚:別再用DataInputStream的readLine( )(編譯時會警告你這個方法已經"過時了(deprecated)"),要用就用BufferedReader的。此外,DataInputStream仍然是I/O類庫的"種子選手"。

為了讓向PrintWriter的過渡變得更簡單,PrintWriter除了有一個拿Writer做參數的構造函數之外,還有一個拿OutputStream做參數的構造函數。但是PrintWriter格式上並不比PrintStream的更好;它們的接口實際上是完全相同的。

PrintWriter的構造函數裡還有一個可選的,能自動地進行清空操作的選項。如果你設了這個標記,那麼每次println( )之後,它都會自動清空。

沒變過的類

Java從1.0升到1.1時,有幾個類沒有變過:

在Java 1.1 中無相對應的類的 Java 1.0 的類DataOutputStreamFileRandomAccessFileSequenceInputStream

特別是DataOutputStream,用法都一點沒變,所以你就可以用InputStream和OutputStream來讀寫可以傳輸的數據了。

自成一派: RandomAccessFile

RandomAccessFile是用來訪問那些保存數據記錄的文件的,這樣你就可以用seek( )方法來訪問記錄,並進行讀寫了。這些記錄的大小不必相同;但是其大小和位置必須是可知的。

首先,你可能會不太相信,RandoMaccessFile竟然會是不屬於InputStream和OutputStream類系的。實際上,除了實現DataInput和DataOutput接口之外(DataInputStream和DataOutputStream也實現了這兩個接口),它和這兩個類系毫不相干,甚至都沒有用InputStream和OutputStream已經准備好的功能;它是一個完全獨立的類,所有方法(絕大多數都只屬於它自己)都是從零開始寫的。這可能是因為RandomAccessFile能在文件裡面前後移動,所以它的行為與其它的I/O類有些根本性的不同。總而言之,它是一個直接繼承Object的,獨立的類。

基本上,RandoMaccessFile的工作方式是,把DataInputStream和DataOutputStream粘起來,再加上它自己的一些方法,比如定位用的getFilePointer( ),在文件裡移動用的seek( ),以及判斷文件大小的length( )。此外,它的構造函數還要一個表示以只讀方式("r"),還是以讀寫方式("rw")打開文件的參數 (和C的fopen( )一模一樣)。它不支持只寫文件,從這一點上看,假如RandomAccessFile繼承了DataInputStream,它也許會干得更好。

只有RandomAccessFile才有seek方法,而這個方法也只適用於文件。BufferedInputStream有一個mark( )方法,你可以用它來設定標記(把結果保存在一個內部變量裡),然後再調用reset( )返回這個位置,但是它的功能太弱了,而且也不怎麼實用。

RandomAccessFile的絕大多數功能,如果不是全部的話,已經被JDK 1.4的nio的"內存映射文件(memory-mapped files)"給取代了。下面我們會講到這部分內容的。

常見的I/O流的使用方法

雖然I/O流的組合方式有很多種,但最常用的也就那麼幾種。下

輸入流

第一到第四部分演示了如何創建和使用InputStream。第四部分還簡單地演示了一下OutputStream的用法。

1. 對輸入文件作緩沖

要想打開打開文件讀取字符,你得先用String或File對象創建一個FileInputReader。為了提高速度,你應該對這個文件作緩沖,因此你得把FileInputReader的reference交給BufferedReader。由於BufferedReader也提供了readLine( )方法,因此它就成為你最終要使用的那個對象,而它的接口也成為你使用的接口了。當你讀到了文件的末尾時,readLine( )會返回一個null,於是就退出while循環了。

最後,用close( )來關閉文件。單從技術角度上說,程序退出的時候(不管有沒有垃圾要回收)都應該調用finalize( ),而finalize( )又會調用close( )。不過各種JVM的實現並不一致,所以最好還是明確地調用close( )。

System.in是一個InputStream,而BufferedReader需要一個Reader作參數,所以要先通過InputStreamReader來轉轉手。

2. 讀取內存

(StringReader的)read( )方法會把讀出來的byte當作int,所以要想正常打印的話,你得先把它們轉換成char。

3. 讀取格式化的內存

要想讀取"格式化"的數據,你就得用DataInputStream了,它是一個面向byte的I/O類 (不是面向char的),因此你只能從頭到底一直用InputStream了。當然你可以把所有東西(比方說文件) 都當成byte,然後用InputStream讀出來,但這裡是String。要想把String變成成byte數組,可以用String的getBytes( )方法,而ByteArrayInputStream是可以處理byte數組的。到了這一步,你就不用擔心沒有合適的InputStream來創建DataInputStream了。

如果你是用readByte( )逐字節地讀取DataInputStream的話,那麼無論byte的值是多少,都是合法的,所以你無法根據返回值來判斷輸入是否已經結束了。你只能用available( )來判斷還有多少字符。

注意,available( )的工作方式會隨讀取介質的不同而不同;嚴格地講,它的意思是"可以不被阻塞地讀取的字節的數目。"對文件來說,它就是整個文件,但如果是其它流,情況就不一定了,所以用之前要多留一個心眼。

你也可以像這樣,用異常來檢查輸入是不是完了。但不管怎麼說,把異常當成控制流程來用總是對這種功能的濫用。

4. 讀取文件

(試試把BufferedWriter去掉,你就能看到它對性能的影響了—— 緩沖能大幅提高I/O的性能)。

LineNumberInputStream這是一個傻乎乎的,沒什麼用的類

輸入流用完之後,readLine( )會返回null。如果寫文件的時候不調用close( ),它是不會去清空緩沖區的,這樣就有可能會落下一些東西了。

輸出流

根據寫數據的方式不同,OutputStream主要分成兩類;一類是寫給人看的,一類是供DataInputStream用的。雖然RandomAccessFile的數據格式同DataInputStream和DataOutputStream的相同,但它不屬於OutputStream的。

5. 存儲和恢復數據

PrintWriter會對數據進行格式化,這樣人就能讀懂了。但是如果數據輸出之後,還要恢復出來供其它流用,那你就必須用DataOutputStream來寫數據,再用DataInputStream來讀數據了。當然,它們可以是任何流,不過我們這裡用的是一個經緩沖的文件。DataOutputStream和DataInputStream是面向byte的,因此這些流必須都是InputStream和OutputStream。

如果數據是用DataOutputStream寫的,那麼不管在哪個平台上,DataInputStream都能准確地把它還原出來。這一點真是太有用了,因為沒人知道誰在為平台專屬的數據操心。如果你在兩個平台上都用Java,那這個問題就根本不存在了 。

用DataOutputStream寫String的時候,要想確保將來能用DataInputStream恢復出來,唯一的辦法就是使用UTF-8編碼,也就是像例程第5部分那樣,用writeUTF( )和readUTF( )。UTF-8是Unicode的一種變形。Unicode用兩個字節來表示一個字符。但是,如果你處理的全部,或主要是ASCII字符(只有7位),那麼無論從存儲空間還是從帶寬上看,就都顯得太浪費了,所以UTF-8 用一個字節表示ASCII字符,用兩或三個字節表示非ASCII的字符。此外,字符串的長度信息存在(字符串)的頭兩個字節裡。writeUTF( )和readUTF( )用的是Java自己的UTF-8版本,所以如果你要用一個Java程序讀取writeUTF( )寫的字符串的話,就必須進行一些特殊處理了。

有了writeUTF( )和readUTF( ),你就能放心地把String和其它數據混在一起交給DataOutputStream了,因為你知道String是以Unicode的形式存儲的,而且可以很方便地用DataOutputStream恢復出來。

writeDouble( )會往流裡寫double,而它"影子"readDouble( )則負責把它恢復出來(其它數據也有類似的讀寫方法)。但是要想讓讀取方法能正常工作,你就必須知道流的各個位置上都放了些什麼數據。因為你完全可以把double讀成byte,char,或其它什麼東西。所以要麼以固定的格式寫文件,要麼在文件裡提供額外的解釋信息,然後一邊讀數據一邊找數據。先提一下,對於復雜數據的存儲和恢復,對象的序列化可能會比較簡單。

6. 讀寫隨機文件

正如我們前面所講的,如果不算它實現了DataInput和DataOutput接口,RandomAccessFile幾乎是完全獨立於其它I/O類庫之外的,所以它不能與InputStream和OutputStream合起來用。雖然把ByteArrayInputStream當作"隨機存取的元素(random-access element)"是一件很合情合理的事,但你只能用RandoMaccessFile來打開文件。而且,你只能假定RandomAccessFile已經做過緩沖了,因為即便沒做你也無能為力。

構造函數的第二個參數的意思是:是以只讀("r") 還是讀寫("rw")方式打開RandomAccessFile。

RandomAccessFile的用法就像是DataInputStream和DataOutputStream的結合(因為它們的接口是等效的)。此外,你還能用seek( )在文件裡上下移動,並進行修改。

隨著JDK 1.4的new I/O的問世,你該考慮一下是不是用"內存映射文件(memory-mapped file)"來代替RandomAccessFile了。

管道流

這一章只會大致地提一下PipedInputStream,PipedOutputStream,PipedReader和PipedWriter。這並不是說它們不重要,只是因為管道流是用於線程間的通信的,所以除非你已經理解了多線程,否則是不會理解它的價值的。我們會在第13章用一個例子來講解這個問題。

讀寫文件的實用程序

把文件讀進內存,改完,再寫文件。這是再普通不過的編程任務了。但是Java的I/O就是有這種問題,即便是做這種常規操作,你也必須寫一大串代碼——根本就沒有輔助函數。更糟的是,那些喧賓奪主的decorator會讓你忘了該怎樣打開文件。因此比較明智的做法還是自己寫一個輔助類。下面就是這樣一個類,它包含了一些"能讓你將文本文件當作字符串來讀寫"的static方法。此外,你還可以創建一個"會把文件的內容逐行存入ArrayList的"TextFile類,(這樣在處理文件的時候,就能使用ArrayList的功能了):

//: com:bruceeckel:util:TextFile.java// Static functions for reading and writing text files as// a single string, and treating a file as an ArrayList.// {Clean: test.txt test2.txt}package com.bruceeckel.util;import java.io.*;import java.util.*;public class TextFile extends ArrayList { // Tools to read and write files as single strings: public static String read(String fileName) throws IOException { StringBuffer sb = new StringBuffer(); BufferedReader in = new BufferedReader(new FileReader(fileName)); String s; while((s = in.readLine()) != null) { sb.append(s); sb.append("\n"); } in.close(); return sb.toString(); } public static void write(String fileName, String text) throws IOException { PrintWriter out = new PrintWriter( new BufferedWriter(new FileWriter(fileName))); out.print(text); out.close(); } public TextFile(String fileName) throws IOException { super(Arrays.asList(read(fileName).split("\n"))); } public void write(String fileName) throws IOException { PrintWriter out = new PrintWriter( new BufferedWriter(new FileWriter(fileName))); for(int i = 0; i < size(); i++) out.println(get(i)); out.close(); } // Simple test: public static void main(String[] args) throws Exception { String file = read("TextFile.Java"); write("test.txt", file); TextFile text = new TextFile("test.txt"); text.write("test2.txt"); }} ///:~

所有這些方法都會直接往外面拋IOException。由於一行讀出來之後,後面的換行符就沒了,因此read( )會在每行的後面再加一個換行符,然後接到StringBuffer的後面(出於效率考慮)。最後它會返回一個"含有整個文件的內容"的String。write( )的任務是打開文件,然後往裡面寫東西。任務完成之後要記著把文件給close( )了。

(TextFile)的構造函數用read( )方法將文件轉化成String,然後用String.split( )和換行符轉換成數組(如果你要經常使用這個類,或許應該重寫一遍構造函數以提高性能)。此外,由於沒有相應的"join"方法,非static的write( )方法只能手動地逐行打印文件。

為了確保它能正常工作,main( )作了一個基本測試。雖然它只是個小程序,但到後面你就會發覺,它卻能幫你節省很多時間,同時讓生活變得輕松一點。

標准I/O

"標准I/O"是Unix的概念,它的意思是,一個程序只使用一個信息流(這種設計思想也以某種形式體現在Windows及其它很多操作系統上)。所有輸入都是從"標准輸入"進來的,輸出都從"標准輸出"出去,錯誤消息都送到"標准錯誤"裡。標准I/O的優點是,它可以很容易地把程序串連起來,並且把一個程序的輸出當作另一個程序的輸入。這是一種非常強大的功能。

讀取標准輸入

Java遵循標准I/O的模型,提供了Syetem.in,System.out,以及System.err。本書一直都在用System.out往標准輸出上寫,而它(System.out)是一個已經預先處理過的,被包裝成PrintStream的對象。和System.out一樣,System.err也是一個PrintStream,但是System.in就不對了,它是一個未經處理的InputStream。也就是說,雖然你可以直接往System.out和System.err上寫,但是要想讀System.in的話,就必須先做處理了。

通常情況下,你會用readLine( )一行一行地讀取輸入,因此要把System.in包裝成BufferedReader。但在這之前還得先用InputSteamReader把System.in轉換成Reader。

將System.out轉換成PrintWriter

System.out是PrintStream,也就是說它是OutputStream。不過PrintWriter有一個能將OutputStream改造成PrintWriter的構造函數。有了這個構造函數,你就可以隨時將System.out轉化成PrintWriter了:

為了啟動自動清空緩沖區的功能,一定要使用雙參數版的構造函數,並且把第二個參數設成true。這點非常重要,否則就有可能會看不到輸出了。

標准I/O的重定向

Java的System類還提供了幾個能讓你重定向標准輸入,標准輸出和標准錯誤的靜態方法:

setIn(InputStream)setOut(PrintStream)setErr(PrintStream)

如果程序在短時間內輸出了大量的信息,使得翻屏的速度非常快,以致於你都沒法讀了,這時對輸出進行重定向就會顯得非常有用了 。對於那些要重復測試用戶輸入的命令行程序來說,對輸入進行重定向也是非常重要的。

I/O重定向處理的不是character流,而是byte流,因此不能用Reader和Writer,要用InputStream和OutputStream。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved