第十八章、Java的I/O系統
對程序語言的設計者來說,創建一個好的I/O系統是一個艱難的任務:
- 需要涵蓋不同的I/O的來源端與想要與之通信的接收端:文件、控制檯、網絡鏈接等。
- 需要涵蓋不同的通信方式:順序、隨機存取、緩衝、二進制、按字符、按行、按字等。
1. File類
File(文件)既能代表一個特定文件名稱,又能代表一個目錄下的一組文件的名稱。如果是文件集,可以對此集合調用list()方法,返回一個字符數組。
1.1 目錄列表
查看一個文件目錄的方法(使用File類):
- f.isFile() 判斷File是否為文件
- f.isDirectory() 判斷File是否為文件夾
- f.list() 返回某個File下的所有文件和目錄的文件名,返回的是String數組
- f.listFiles() 返回某個File下所有文件和目錄的絕對路徑,返回的是File數組
1.2 目錄實用工具
在文件集上執行操作:
- Directory.local()
- Directory.walk()
1.3 目錄的檢查:
- f.canRead() 判斷File是否可讀
- f.canWrite() 判斷File是否可寫
- f.getName() 獲取文件的名字
- f.getParent() 獲取父目錄文件路徑
- f.getPath() 獲取File路徑
- f.length() 獲取File長度
- f.lastModified() 獲取文件上次被修改時間
- f.exists() 判斷文件是否存在
1.4 文件的操作
- File file=new File() 創建文件對象
- file.createNewFile() 創建文件
- file.mkdir() 創建目錄
- file.delete() 刪除目錄
- file.renameTo(newFile) 重命名(或移動)文件到新的目錄&文件名位置
2. 輸入和輸出
- “流”:編程語言的I/O庫經常使用“流”,它代表任何有能力產生數據的數據源對象或者有能力接受數據的接收端對象。
- “流”:屏蔽了實際的I/O設備處理數據的細節(封裝具體底層&設備實現?)。
Java類庫中的I/O類分成輸入和輸出兩部分:
- 輸入:通過繼承,任何自Inputstream或Reader派生而來的類都含有名為read()的基本方法,用於讀取單個字節或者字節數組。
- 輸出:同樣的OutputStream或者Writer派生出來的類都含有名為write()的基本方法,用於寫單個字節或者字節數組。
- 總結:但是不會用到(上述方法read()和write()),它們之所以存在是因為別的類可以使用它們以便提供更有用的接口。因此,很少使用單一的類來創建流對象,而是通過疊合多個對象來提供所期望的功能。
2.1 InputStream類型
InputStream的作用是用來表示從不同數據源產生輸出的類,這些數據源(均為InputStream的子類)包括:
- 字節數組 ByteArrayInputStream
- String對象 StringBufferInputStream
- 文件 FileInputStream
- 管道 PipedInputStream:工作方式與實際管道相似,即從一段輸入,從另一端輸出
- 序列 SequenceInputStream 一個由其他種類的流組成的序列,以便可以將它們收集合併到一個流內
- 其他數據源,如Internet鏈接等
- 特殊派生類:FilterInputStream也屬於一種InputStream,為裝飾器類提供的基類,其中,裝飾器類可以把屬性或有用的接口與輸入流連接在一起。
2.2 OutputStream類型
該類別的類決定了輸出所要去往的目標,包含:
- 字節數組 ByteArrayOutputStream
- 文件 FileOutputStream
- 管道 PipedOutputStream
- 特殊派生類:FilterOutputStream為裝飾器類提供了一個基類,裝飾器類把屬性或者有用的接口與輸出流連接了起來:
3. 添加屬性和有用的接口
- Java I/O類庫裡存在filter(過濾器)類的原因所在抽象類filter是所有裝飾器類的基類。
- FilterInputStream和FilterOutputStream:是用來提供裝飾類器接口以及控制特定輸入流和輸出流的兩個類。FilterInputStream和FilterOutputStream分別自I/O類庫中的基類InputStream和OutputStream派生而來,這兩個類是裝飾器的必要條件。
裝飾者(GoF23之一):動態的將功能附加到對象上,在對象擴展方面,它比繼承更加有彈性。
3.1 通過FilterInputStream從InputStream讀取數據
FilterInputStream類能夠完成完全不同的事情,其中,DateInputStream允許讀取不同的基本類型數據以及String對象。
其他FilterInputStream類則在內部修改InputStream的行為方式:是否緩衝,是否保留它所讀過的行(允許查詢行數或設置行數),以及是否把單一字符推回輸入流。
3.2 通過FilterOutputStream向OutputStream導入
4. Reader和Writer
InputStream和OutputStream在以面向字節形式的IO中可以提供極有價值的功能,Reader和Writer(Java 1.1對基礎IO流類庫進行了重大修改,可能會以為是用來替換InputStream和OutputStream的)則提供兼容Unicode和麵向字符的IO功能。
- Java 1.1向InputStream和OutputStream繼承層次中添加了一些新類,所以這兩個類不會被取代。
- 有時必須把來自於字節層次結構中的類和字符層次中的類結合起來。為了實現這個目的,要用到適配器類:InputStreamReader可以吧InputStream轉換為Reader,而OutputStreamWriter可以吧OutputStream轉換為Writer。
適配器(GoF23之一):將一個類的接口轉換成客戶端希望的另一個接口。
設計Reader和Writer繼承層次結構只要是為了國際化。老的IO流繼承層次結構僅支持8位字節流,並且不能很好地處理16位的Unicode字符。所以Reader和Writer繼承層次結構就是為了在所有IO操作中都支持Unicode。
4.1 數據的來源和去處
4.2 更改流的行為
對於InputStream和OutputStream來說,有裝飾器子類來修改流以滿足需要。Reader和Writer的類繼承層次結構繼續沿用相同的思想——但不完全相同。
無論何時使用readLine(),都不應該使用DataInputStream,而應該使用BufferedReader。
為了更容易地過渡到使用PrintWriter,它提供了一個既接受Writer對象又能接受任何OutputStream對象的構造器。PrintWriter的格式化接口實際上與PrintStream相同。
5. 自我獨立的類:RandomAccessFile
RandomAccessFile適用於由大小已知的記錄組成的文件,所以可以使用seek()將記錄從一處轉移到另一處,然後讀取或者修改記錄。文件中記錄的大小不一定都相同,只要能夠確定那些記錄有多大以及它們在文件中的位置即可。
RandomAccessFile實現了DataInput和DataOutput接口,它是一個完全獨立的類,從頭開始編寫其所有的方法(大多數都是本地的)。這麼做是因為RandomccessFile擁有和別的I/O類型本質不同的行為,因為可以在一個文件內向前和向後移動。在任何情況下,它都是自我獨立的,直接從Object派生而來。
方法getFilePointer()用於查找當前所處的文件位置,seek()用於在文件內移至新的位置,length()用於判斷文件的最大尺寸。另外,其構造器還需要第二個參數(和C中的fopen()相同)用來指示我們只是“隨機讀”®還是“既讀又寫”(rw)。
6. I/O流的典型使用方式
6 I/O流的典型使用方式(Typical uses of I/O streams)
儘管可以通過不同的方式組合I/O流類,但我們可能也就只用到其中的幾種組合。下面的例子可以作為
6.1 緩衝輸入文件
使用以String或File對象作為文件名的FileInputReader。為了提高速度,對文件進行緩衝,將所產生的引用傳給一個BufferedReader構造器。
BufferedReader in = new BufferedReader(new FileReader(filename));
6.2 從內存輸入
從BufferedInputFile.read()讀入的String結果被用來創建一個StringReader。然後調用read()每次讀取一個字符,併發送到控制檯。
StringReader in = new StringReader(BufferedInputFile.read("MemoryInput.java"));
6.3 格式化的內存輸入
要讀取格式化數據,可以使用DataInputStream,它是面向字節的IO類,因此必須用InputStream而不是Reader。
<code>DataInputStream in = new DataInputStream(new ByteArrayInputStream(BufferedInputFile.read("FormattedMemoryInput.java").getBytes()));DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(“TestEOF.java”)));/<code>
6.4 基本文件輸出
FileWriter對象可以向文件寫入數據。通常會用BufferedWriter將其包裝起來用以緩衝輸出。
<code>BufferedReader in = new BufferedReader(new StringReader( BufferedInputFile.read("BasicFileOutput.java")));PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)));/<code>
6.5 存儲和恢復數據
PrintWriter可以對數據進行格式化,以便閱讀。但是為了輸出可供另一個流恢復的數據,需要用
<code>DataOutputStream寫入數據:DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(“Data.txt”)));DataInputStream恢復數據:DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(“Data.txt”)));/<code>
6.6 讀寫隨機訪問文件
RandomAccessFile rf = new RandomAccessFile(file, “rw”);
7. 文件讀寫的使用工具
讀取文件:
BufferedReader in = new BufferedReader(new FileReader((new File(fileName)).getAbsoluteFile()));
寫入文件:
PrintWriter out = new PrintWriter((new File(fileName)).getAbsoluteFile());
8. 標準IO
標準IO源自於Unix的“程序所使用的單一信息流”這一概念。程序的所有輸入都可以來自於標準輸入,所有輸出都可以發送到標準輸出。
8.1 從標準輸入中讀取
- 標準輸入:System.in未加工的InputStream
- 標準輸出:System.out PrintStream對象
- 標準錯誤:System.err PrintStream對象
通常會用readLine()一次一行讀取輸入,將System.in包裝城BufferedReader來使用,這要求必須用InputStreamReader把Sytem.in轉換成Reader。System,in通常應該對它進行緩衝。
8.2 將System.out轉換成PrintWriter
PrintWriter out = new PrintWriter(System.out, true);
8.3 標準IO重定向
Java的System類提供了靜態方法嗲用,以允許對標準輸入輸出和錯誤IO流進行重定向:
- setIn(InputStream)
- setOut(PrintStream)
- setErr(PrintStream)
9.進程控制
Java內部執行其他操作系統的程序,並且控制這些程序輸入輸出,Java類庫提供了執行這些操作的類:ProcessBuilder對象
10. 新I/O(java.nio.*)
JDK 1.4的java.nio.*包中引入了新的IO類庫,其目的在於提高速度。實際上,舊的IO包已經使用nio重新實現過,以便充分利用這種速度提高。
速度提高源自於所使用的結構更接近於操作系統執行IO的方式:通道和緩衝器
唯一直接與通道交互的緩衝器是ByteBuffer,可以存儲未加工字節的緩衝器。java.nio.ByteBuffer是相當基礎的類:通過稿子分配多少存儲空間來創建一個ByteBuffer對象,並且還有一個方法選擇集,用於以原始的字節形式或基本數據類型輸出和讀取數據。但是,沒辦法輸出或讀取對象,即使是字符串對象也不行。這種處理雖然很低級,但卻正好,因為這是大多數草走系統中更有效的映射方式。
舊IO類庫有三個類被修改了,用以產生FileChannel。這三個被修改類是FileInputStream、FileOutputStream以及用於既讀又寫的RandomAccessFile。這些都是字節操作流,與底層nio性質一致。Reader和Writer這些字符模式類不能用於產生通道;但是java.nio.channels.Channels類提供了使用方法,用於在通道中產生Reader和Writer。
10.1 轉換數據
在GetChannel.java中,必須每次只讀取一個字節的數據,然後將每個byte類型強制轉換成char類型。而java.nio.CharBuffer有一個toString方法:返回一個包含緩衝器中所有字符的字符串。
10.2 獲取基本類型
儘管ByteBuffer只能保存字節類型數據,但是它可以從其所容納的字節中產生出各種不同的基本類型值的方法
bb.asCharBuffer();
bb.asShortBuffer();
bb.asIntBuffer();
bb.asLongBuffer();
bb.asFloatBuffer();
bb.asDoubleBuffer();
10.3 視圖緩衝器
視圖緩衝器(view buffer)可以讓我們通過某個特定的基本數據類型的視窗查看其底層的ByteBuffer。對視圖的任何修改都會映射成為對ByteBuffer中數據的修改。
先用重載後的put()方法存儲一個整數數組。接著get()和put()方法調用直接訪問底層ByteBuffer中的某個整數位置。注意,這些通過直接與ByteBuffer對話訪問絕對位置的方式也同樣適用於基本類型。
一旦底層的ByteBuffer通過視圖緩衝器填滿了整數或其他基本類型時,就可以直接被寫到通道中。正像從通道中讀取那樣容易,然後使用視圖緩衝器可以把任何數據都轉化為某一特定的基本類型。
10.4 用緩衝器操縱數據
此圖闡明瞭nio類之間的關係,便於理解怎麼移動和轉換數據。如果想把一個字節數組寫到文件中去,那麼就應該使用ByteBuffer.wrap()方法把字節數組包裝起來,然後用getChannel()方法在FileOutputStream上打開一個通道,接著將來自於ByteBuffer的數據寫到FileChannel。
注意:BytBuffer是將數據移進移出通道的唯一方式,並且只能創建一個獨立的基本類型緩衝器,或者使用as方法從ByteBuffer中獲得。也就是說,不能把基本類型的緩衝器轉換成ByteBuffer。
10.5 緩衝器的細節
Buffer有數據和可以高效地訪問及操作這些數據的四個索引組成,mark(標記)、position(位置)、limit(界限)和capacity(容量)。
10.6 內存映射文件(MemoryMappedFile)
內存映射文件允許創建和修改因為太大而不能放入內存的文件。可以假定整個文件都放在內存中,而且可以完全把它當作非常大的數組訪問
MappedByteBuffer out = new RandomAccessFile("test.dat", "rw").getChannel()
.map(FileChannel.MapMode.READ_WRITE, 0, length);
10.6.1 性能
nio實現後性能有所提高,但是映射文件訪問往往可以更加顯著地加快速度。
10.7 文件加鎖
JDK 1.4引入了文件加鎖機制,允許同步訪問某個做為共享資源的文件。文件鎖對其他的操作系統進程是可見的,因為Java的文件加鎖直接映射到本地操作系統的加鎖工具。
FileOutputStream fos= new FileOutputStream("file.txt");
FileLock fl = fos.getChannel().tryLock();
11. 壓縮
Java IO類庫中的類支持讀寫壓縮格式的數據流。 Java IO類庫中的類支持讀寫壓縮格式的數據流。
11.1 用GZIP進行簡單壓縮
如果相對單一數據流進行壓縮,GZIP接口是比較合適的選擇:
壓縮:BufferedOutputStream
解壓:BufferedReader
11.2 用Zip進行多文件保存
支持Zip格式的Java庫更加全面,它顯示了用Checksum類來計算和校驗文件的校驗和的方法。一共有兩種Checksum類型:Adler32(快)和CRC32(慢,準確)。
壓縮:BufferedOutputStream
解壓: BufferedReader
11.3 Java檔案文件(JAR)
JAR(Java ARchive,Java檔案文件)文件格式:
將一組文件壓縮到單個壓縮文件中。同Java中任何其他東西一樣,JAR也是跨平臺的。由於採用壓縮技術,可以使傳輸時間更短,只需向服務器發送一次請求即可。
12 對象序列化(Android ->Parcelable)
對象序列化的相關概念:
- Java的對象序列化&反序列化:將那些實現了Serializable接口的對象轉換成一個字節序列,並能夠在以後將這個字節序列完全恢復為原來的對象。
- 跨平臺特性:這一過程甚至可通過網絡進行;這意味著序列化機制能自動彌補不同操作系統之間的差異。也就是說,可以在運行Windows系統的計算機上創建一個對象,將其序列化,通過網絡將它發送給一臺運行Unix系統的計算機,然後在那裡誰確地重新組裝,而卻不必擔心數據在不同機器上的表示會不同,也不必關心字節的順序或者其他任何細節。
- 輕量級持久性(lightweight persistence):利用序列化可以實現輕量級持久性(lightweight persistence)。“持久性”意味著一個對象的生存週期並不取決於程序是否正在執行;它可以生存於程序的調用之間。
對象序列化的概念加入到語言中是為了支持兩種主要特性。
- RMI場景: 一是Java的遠程方法調用(Remote Method Invocation, RMI),它使存活於其他計算機上的對象使用起來就像是存活於本機上一樣。
- 對Java Beans來說,對象的序列化也是必需的。使用一個Bean時,一般情況下是在設計階段對它的狀態信息進行配置。這種狀態信息必須保存下來,並在程序啟動時進行後期恢復,這種具體工作就是由對象序列化完成的。
對象序列化的實現:
- 應用側的實現:只要對象實現了Serializable接口(該接口僅是一個標記接口,不包括任何方法),對象的序列化處理就會非常簡單。class Data implements Serializable
- 平臺側的實現:當序列化的概念被加入到語言中時,許多標準庫類都發生了改變,以便具備序列化特性一其中包括所有基 本數據類型的封裝器、所有容器類以及許多其他的東西。甚至Class對象也可以被序列化。
底層實現步驟:
- 序列化一個對象:首先要創建某些OutputStream對象,然後將其封裝在一個ObjectOutputStream對象內。這時,只需調用writeObject0即可將對象序列化,並將其發送給OutputStream(對象化序列是基於字節的,因要使用InputStream和OutputStream繼承層次結構)。
- 反序列化一個對象:要反向進行該過程(即將-一個序列還原為一個對象),需要將個InputStream封裝在ObjectmputStream內,然後調用readObject()。和往常一樣,我們最後獲得的是一個引用,它指向一個向上轉型的Object,所以必須向下轉型才能直接設置它們。
12.1 尋找類
將一個對象從它的序列化狀態中恢復出來,哪些工作是必須的?
- 舉例:網絡側傳輸過來的序列化數據,本地如何反序列化?
- 前置條件:必須保證JVM找得到對應對象的***.class文件,否則反序列化時,會throw ClassNotFoundException。
12.2 序列化的控制
12.1. 1 Serializable接口
- 應用場景:默認序列化機制。
12.1. 2 Exterbalizable(implements Serializable)接口
- 應用場景:默認序列化機制(Serializable)並不難操縱。如果希望部分序列化或子對象不必序列化。可通過Exterbalizable接口代替Serializable。
- 新增方法:Exterbalizable接口繼承了Serializable,同時增添了兩個方法:writeExternal()和readExternal()。這兩個方法會在序列化和反序列化還原的過程中被自動調用。
12.1. 3 Serializable對象與Externalizable對象的區別:
- Serializable對象:對於Serializable對象來說,對象完全以它存儲的二進制位為基礎進行構造,而不調用構造器。
- Externalizable對象:對於Externalizable對象,所有的普通的默認構造器都會被調用(包括字段定義時的初始化),然後調用readeExternal()。必須注意:所有的默認構造器都被調用之後,才能使Externalizable對象產生正確的行為。
12.1. 4 transient(瞬時)關鍵字
- 應用場景:特定子對象不想讓java的序列化自動保存與恢復,可以使用transient逐個字段地關閉序列化。
- 場景舉例:如對象中包含了敏感信息的字段(比如用戶密碼)時
- 實現結果:當對象被恢復時,transient的password域就會變成null。雖然toString()是用重載後的+運算符來連接String對象,但是null引用會被自動轉換成字符串null。
12.1. 5 Externalizable的替代方法
Externalizable的替代方案1:
- 方案原理:可以實現Serializable接口,並添加(非覆蓋或實現)名為writeObject()和readObject()方法。這樣一旦對象被序列化或者被反序列化還原,就會自動地分別調用這個方法。也就是說,只要提供這兩個方法,就會使用它們而不是默認的序列化機制。
- 具體實現:實際上並不是從這個類的其他方法中調用它們,而是ObjectOutputStream和ObjectInputStream對象的writeObject()和readObject()方法調用了對象的writeObject()和readObject()方法(類型信息章節展示瞭如何在類的外部訪問private方法)。
private void writeObject(ObjectOutputStream stream)throws IOException;
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException;
Externalizable的替代方案2:
- 還有另一個技巧,可在自己的writeObject()內部,調用defaultWriteObject()來選擇執行默認的writeObject().
12.1. 6 深度拷貝&序列化
- 深度拷貝:deep copy,表示複製的是整個對象網,而不僅僅是基本類型對象和對象引用;
- 實現方法:通過對字節數組使用對象序列化,可實現深度拷貝。
13 XML(android *.xml 即使用此機制)
- 對象序列化的一個重要限制是它只是Java解決方案:只有Java程序才能反序列化這種對象。
- Xml格式:一種更具有互操作性的解決方案是將數據轉換為XML格式,這樣可以使其被各種各樣的平臺和語言使用。
14 Preferences (Android ->SharedPreference)
- Preferences API與對象序列化相比,前者與對象持久性更密切。它可以自動存儲和讀取信息。
- 不過,它只能用於小的受限的數據集合——只能存儲基本數據和字符串,並且每個字符串的存儲長度不能超過8K。顧名思義,Preferences API用於存儲和讀取用戶的偏好preferences以及程序配置項的設置。
- Preferences是一個鍵-值集合,存儲在一個節點層次結構中。
15 總結:
- Java I/O流類庫:確能滿足我們的基本需求:可以通過控制檯、文件、內存塊、甚至因特網進行讀寫。
- 裝飾器模式:在此章節的大量運用,提供了應用的巨大靈活性。
閱讀更多 編程家園 的文章