TIJ閱讀筆記(第十二章)

來源:互聯網
上載者:User
筆記 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( )裡面用Regex(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。


相關文章

Beyond APAC's No.1 Cloud

19.6% IaaS Market Share in Asia Pacific - Gartner IT Service report, 2018

Learn more >

Apsara Conference 2019

The Rise of Data Intelligence, September 25th - 27th, Hangzhou, China

Learn more >

Alibaba Cloud Free Trial

Learn and experience the power of Alibaba Cloud with a free trial worth $300-1200 USD

Learn more >

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。