Java編程思想 Java I/O系統總結

File類

目錄列表器
假設我們想查看一個目錄列表 可以用兩種方法來使用File對象 如果我們調用不帶參數的list()方法 便可以獲得此File對象包含的全部列表 然而 如果我們想獲得一個受限列表 例如 想得到所有擴展名爲.java的文件 那麼我們就要用到 目錄過濾器 這個類會告訴我們怎樣顯示符合條件的File對象
下面是一個示例 注意 通過使用java.utils.Arrays.sort()和String.CASE_INSENSITIVE.ORDERComparator 可以很容易地對結果進行排序(按字母順序)
在這裏插入圖片描述

匿名內部類
這個例子很適合用一個匿名內部類進行改寫 首先創建一個filter()方法 它會返回一個指向FilenameFilter的引用
在這裏插入圖片描述

這個設計有所改進 因爲現在FilenameFilter類緊密地和DirList2綁定在一起 然而 我們可以進一步修改該方法 定義一個作爲list()參數的匿名內部類 這樣一來程序會變得更小
在這裏插入圖片描述

目錄實用工具
程序設計中一項常見的任務就是在文件集上執行操作 這些文件要麼在本地目錄中 要麼遍佈於整個目錄樹中 如果有一種工具能夠爲你產生這個文件集 那麼它會非常有用 下面的使用工具類就可以通過使用local()方法產生由本地目錄中的文件構成的File對象數組 或者通過使用walk()方法產生給定目錄下的由整個目錄樹中所有文件構成的List(File對象比文件名更有用 因爲File對象包含更多的信息) 這些文件是基於你提供的正則表達式而被選中的
在這裏插入圖片描述
在這裏插入圖片描述

TreeInfo.toString()方法使用了一個 靈巧打印機 類 以使輸出更容易瀏覽 容器默認的toString()方法會在單個行中打印容器中的所有元素 對於大型集合來說 這會變得難以閱讀 因此你可能希望使用可替換的格式化機制 下面是一個可以添加新行並縮排所有元素的工具
在這裏插入圖片描述
在這裏插入圖片描述

Directory實用工具放在了net.mindview.util包中 以使其可以更容易地被獲得 下面的例子說明了你可以如何使用它的樣本
在這裏插入圖片描述

我們可以更進一步 創建一個工具 它可以在目錄中穿行 並且根據Strategy對象來處理這些目錄中的文件(這是策略設計模式的另一個示例)
在這裏插入圖片描述
在這裏插入圖片描述

目錄的檢查及創建
File類不僅僅只代表存在的文件或目錄 也可以用File對象來創建新的目錄或尚不存在的整個目錄路徑 我們還可以查看文件的特性(如:大小 最後修改日期 讀/寫) 檢查某個File對象代表的是一個文件還是一個目錄 並可以刪除文件 下面的示例展示了File類的一些其他方法(請參考http://java.sun.com上的HTML文檔以全面瞭解它們)
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

輸入和輸出

InputStream類型
InputStream的作用是用來表示那些從不同數據源產生輸入的類 如表18-1所示 這些數據源包括

  1. 字節數組
  2. String對象
  3. 文件
  4. 管道 工作方式與實際管道相似 即 從一端輸入 從另一端輸出
  5. 一個由其他種類的流組成的序列 以便我們可以將它們收集合併到一個流內
  6. 其他數據源 如Internet連接等
    每一種數據源都有相應的InputStream子類 另外 FilterInputStream也屬於一種InputStream 爲 裝飾器(decorator)類提供基類 其中 裝飾器 類可以把屬性或有用的接口與輸入流連接在一起
    在這裏插入圖片描述

OutputStream類型
如表18-2所示 該類別的類決定了輸出所要去往的目標 字節數組(但不是String 不過你當然可以用字節數組自己創建) 文件或管道
另外 FilterOutputStream爲 裝飾器 類提供了一個基類 裝飾器 類把屬性或者有用的接口與輸出流連接了起來
在這裏插入圖片描述

添加屬性和有用的接口

通過FilterInputStream從InputStream讀取數據
我們幾乎每次都要對輸入進行緩衝 不管我們正在連接的是什麼I/O設備 所以 I/O類庫把無緩衝輸入(而不是緩衝輸入)作爲特殊情況(或只是方法調用)就顯得更加合理了 FilterInputStream的類型及功能如表18-3所示
在這裏插入圖片描述

通過FilterOutPutStream向OutputStream寫入
FilterOutPutStream的類型及功能如表18-4所示
在這裏插入圖片描述

Reader和Writer

數據的來源和去處
幾乎所有原始的Java I/O流類都有相應的Reader和Writer類來提供天然的Unicode操作 然而在某些場合 面向字節的InputStream和OutputStream纔是正確的解決方案 特別是 java.util.zip類庫就是面向字節的而不是面向字符的 因此 最明智的做法是儘量嘗試使用Reader和Writer 一旦程序代碼無法成功編譯 我們就會發現自己不得不使用面向字節的類庫
下面的表展示了在兩個繼承層次結構中 信息的來源和去處(即數據物理上來自哪裏及去向哪裏)之間的對應關係
在這裏插入圖片描述

更改流的行爲
對於InputStream和OutputStream來說 我們會使用FilterInputStream和FilterOutputStream的裝飾器子類來修改 流 以滿足特殊需要 Reader和Writer的類繼承層次結構繼續沿用相同的思想 但是並不完全相同
在下表中 相對於前一表格來說 左右之間的對應關係的近似程度更加粗略一些 造成這種差別的原因是因爲類的組織形式不同 儘管BufferedOutputStream是FilterOutputStream的子類 但是BufferedWriter並不是FilterWriter的子類(儘管FilterWriter是抽象類 沒有任何子類 把它放在那裏也只是把它作爲一個佔位符 或僅僅讓我們不會對它所在的地方產生疑惑) 然而 這些類的接口卻十分相似
在這裏插入圖片描述

未發生變化的類
有一些類在Java 1.0和Java 1.1之間則未做改變
在這裏插入圖片描述

自我獨立的類:RandomAccessFile
RandomAccessFile適用於由大小已知的記錄組成的文件 所以我們可以使用seek()將記錄從一處轉移到另一處 然後讀取或者修改記錄 文件中記錄的大小不一定都相同 只要我們能夠確定那些記錄有多大以及它們在文件中的位置即可

I/O流的典型使用方式

緩衝輸入文件
如果想要打開一個文件用於字符輸入 可以使用以String或File對象作爲文件名的FileInputReader 爲了提高速度 我們希望對那個文件進行緩衝 那麼我們將所產生的引用傳給一個BufferedReader構造器 由於BufferedReader也提供readLine()方法 所以這是我們的最終對象和進行讀取的接口 當readLine()將返回null時 你就達到了文件的末尾
在這裏插入圖片描述

從內存輸入
在下面的示例中 從BufferedInputFile.read()讀入的String結果被用來創建一個StringReader 然後調用read()每次讀取一個字符 並把它發送到控制檯
在這裏插入圖片描述

格式化的內存輸入
要讀取格式化數據 可以使用DataInputStream 它是一個面向字節的I/O類(不是面向字符的) 因此我們必須使用InputStream類而不是Reader類 當然 我們可以用InputStream以字節的形式讀取任何數據(例如一個文件) 不過 在這裏使用的是字符串
在這裏插入圖片描述

如果我們從DataInputStream用readByte()一次一個字節地讀取字符 那麼任何字節的值都是合法的結果 因此返回值不能用來檢測輸入是否結束 相反 我們可以使用available()方法查看還有多少可供存取的字符 下面這個例子演示了怎樣一次一個字節地讀取文件
在這裏插入圖片描述

基本的文件輸出
FileWriter對象可以向文件寫入數據 首先 創建一個與指定文件連接的FileWriter 實際上 我們通常會用BufferedWriter將其包裝起來用以緩衝輸出(嘗試移除此包裝來感受對性能的影響 緩衝往往能顯著地增加I/O操作的性能) 在本例中 爲了提供格式化機制 它被裝飾成了PrintWriter 按照這種方式創建的數據文件可作爲普通文本文件讀取
在這裏插入圖片描述

文本文件輸出的快捷方式
Java SE5在PrintWriter中添加了一個輔助構造器 使得你不必在每次希望創建文本文件並向其中寫入時 都去執行所有的裝飾工作 下面是用這種快捷方式重寫的BasicFileOutput.java
在這裏插入圖片描述
在這裏插入圖片描述

存儲和恢復數據
PrintWriter可以對數據進行格式化 以便人們的閱讀 但是爲了輸出可供另一個 流 恢復的數據 我們需要用DataOutputStream寫入數據 並用DataInputStream恢復數據 當然 這些流可以是任何形式 但在下面的示例中使用的是一個文件 並且對於讀和寫都進行了緩衝處理 注意DataOutputStream和DataInputStream是面向字節的 因此要使用InputStream和OutputStream
在這裏插入圖片描述

讀寫隨機訪問文件
在使用RandomAccessFile時 你必須知道文件排版 這樣才能正確地操作它 RandomAccessFile擁有讀取基本類型和UTF-8字符串的各種具體方法 下面是示例
在這裏插入圖片描述
在這裏插入圖片描述

管道流
PipedInputStream PipedOutputStream PipedReader及PipedWriter的價值只有在我們開始理解多線程之後纔會顯現 因爲管道流用於任務之間的通信

文件讀寫的實用工具
下面的TextFile類被用來簡化對文件的讀寫操作 它包含的static方法可以像簡單字符串那樣讀寫文本文件 並且我們可以創建一個TextFile對象 它用一個ArrayList來保存文件的若干行(如此 當我們操縱文件內容時 就可以使用ArrayList的所有功能)
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

讀取二進制文件
這個工具與TextFile類似 因爲它簡化了讀取二進制文件的過程
在這裏插入圖片描述

標準I/O

從標準輸入中讀取
通常我們會用readLine()一次一行地讀取輸入 爲此 我們將System.in包裝成BufferedReader來使用這要求我們必須用InputStreamReader把System.in轉換成Reader 下面這個例子將直接回顯你所輸入的每一行
在這裏插入圖片描述

將System.out轉換成PrintWriter
System.out是一個PrintStream 而PrintStream是一個OutputStream PrintWriter有一個可以接受OutputStream作爲參數的構造器 因此 只要需要 就可以使用那個構造器把System.out轉換成PrintWriter
在這裏插入圖片描述

標準I/O重定向
如果我們突然開始在顯示器上創建大量輸出 而這些輸出滾動得太快以至於無法閱讀時 重定向輸出就顯得極爲有用 對於我們想重複測試某個特定用戶的輸入序列的命令行程序來說 重定向輸入就很有價值 下例簡單演示了這些方法的使用
在這裏插入圖片描述

進程控制
你經常會需要在Java內部執行其他操作系統的程序 並且要控制這些程序的輸入和輸出 Java類庫提供了執行這些操作的類
一項常見的任務是運行程序 並將產生的輸出發送到控制檯 本節包含了一個可以簡化這項任務的實用工具 在使用這個實用工具時 可能會產生兩種類型的錯誤 普通的導致異常的錯誤 對這些錯誤我們只需重新拋出一個運行時異常 以及從進程自身的執行過程中產生的錯誤 我們希望用單獨的異常來報告這些錯誤
在這裏插入圖片描述
要想運行一個程序 你需要向OSExecute.command()傳遞一個command字符串 它與你在控制檯上運行該程序所鍵入的命令相同 這個命令被傳遞給java.lang.ProcessBuilder構造器(它要求這個命令作爲一個String對象序列而被傳遞) 然後所產生的ProcessBuilder對象被啓動
在這裏插入圖片描述
在這裏插入圖片描述
爲了捕獲程序執行時產生的標準輸出流 你需要調用getInputStream() 這是因爲InputStream是我們可以從中讀取信息的流 從程序中產生的結果每次輸出一行 因此要使用readLine()來讀取 這裏這些行只是直接被打印了出來 但是你還可能希望從command()中捕獲和返回它們 該程序的錯誤被髮送到了標準錯誤流 並且通過調用getErrotStream()得以捕獲 如果存在任何錯誤 它們都會被打印並且會拋出OSExecuteException 因此調用程序需要處理這個問題
下面是展示如何使用OSExecute的示例
在這裏插入圖片描述
這裏使用了javap反編譯器(隨JDK發佈)來反編譯該程序

新I/O
舊I/O類庫中有三個類被修改了 用以產生FileChannel 這三個被修改的類是FileInputStream FileOutputStream以及用於既讀又寫的RandomAccessFile 注意這些是字節操縱流 與低層的nio性質一致 Reader和Writer這種字符模式類不能用於產生通道 但是java.nio.channels.Channels類提供了實用方法 用以在通道中產生Reader和Writer
下面的簡單實例演示了上面三種類型的流 用以產生可寫的 可讀可寫的及可讀的通道
在這裏插入圖片描述

一旦調用read()來告知FileChannel向ByteBuffer存儲字節 就必須調用緩衝器上的flip() 讓它做好讓別人讀取字節的準備(是的 這似乎有一點拙劣 但是請記住 它是很拙劣的 但卻適用於獲取最大速度) 如果我們打算使用緩衝器執行進一步的read()操作 我們也必須得調用clear()來爲每個read()做好準備 這在下面這個簡單文件複製程序中可以看到
在這裏插入圖片描述

然而 上面那個程序並不是處理此類操作的理想方式 特殊方法transferTo()和transferFrom()則允許我們將一個通道和另一個通道直接相連
在這裏插入圖片描述

轉換數據
回過頭看GetChannel.java這個程序就會發現 爲了輸出文件中的信息 我們必須每次只讀取一個字節的數據 然後將每個byte類型強制轉換成char類型 這種方法似乎有點原始 如果我們查看一下java.nio.CharBuffer這個類 將會發現它有一個toString()方法是這樣定義的 返回一個包含緩衝器中所有字符的字符串 既然ByteBuffer可以看作是具有asCharBuffer()方法的CharBuffer 那麼爲什麼不用它呢 正如下面的輸出語句中第一行所見 這種方法並不能解決問題
在這裏插入圖片描述
在這裏插入圖片描述
緩衝器容納的是普通的字節 爲了把它們轉換成字符 我們要麼在輸入它們的時候對其進行編碼(這樣 它們輸出時才具有意義) 要麼在將其從緩衝器輸出時對它們進行解碼 可以使用java.nio.charset.Charset類實現這些功能 該類提供了把數據編碼成多種不同類型的字符集的工具
在這裏插入圖片描述
在這裏插入圖片描述

獲取基本類型
儘管ByteBuffer只能保存字節類型的數據 但是它具有可以從其所容納的字節中產生出各種不同基本類型值的方法 下面這個例子展示了怎樣使用這些方法來插入和抽取各種數值
在這裏插入圖片描述
在這裏插入圖片描述

視圖緩衝器
視圖緩衝器(view buffer)可以讓我們通過某個特定的基本數據類型的視窗查看其底層的ByteBuffer ByteBuffer依然是實際存儲數據的地方 支持 着前面的視圖 因此 對視圖的任何修改都會映射成爲對ByteBuffer中數據的修改 正如我們在上一示例看到的那樣 這使我們可以很方便地向ByteBuffer插入數據 視圖還允許我們從ByteBuffer一次一個地(與ByteBuffer所支持的方式相同)或者成批地(放入數組中)讀取基本類型值 在下面這個例子中 通過IntBuffer操縱ByteBuffer中的int型數據
在這裏插入圖片描述
在這裏插入圖片描述

一旦底層的ByteBuffer通過視圖緩衝器填滿了整數或其他基本類型時 就可以直接被寫到通道中了 正像從通道中讀取那樣容易 然後使用視圖緩衝器可以把任何數據都轉化成某一特定的基本類型 在下面的例子中 通過在同一個ByteBuffer上建立不同的視圖緩衝器 將同一字節序列翻譯成了short int float long和double類型的數據
在這裏插入圖片描述
在這裏插入圖片描述
ByteBuffer通過一個被 包裝 過的8字節數組產生 然後通過各種不同的基本類型的視圖緩衝器顯式了出來 我們可以在下圖中看到 當從不同類型的緩衝器讀取時 數據顯式的方式也不同 這與上面程序的輸出相對應
在這裏插入圖片描述

字節存放次序
不同的機器可能會使用不同的字節排序方法來存儲數據 big endian(高位優先)將最重要的字節存放在地址最低的存儲器單元 而 little endian(低位優先)則是將最重要的字節放在地址最高的存儲器單元 當存儲量大於一個字節時 像int float等 就要考慮字節的順序問題了 ByteBuffer是以高位優先的形式存儲數據的 並且數據在網上傳送時也常常使用高位優先的形式 我們可以使用帶有參數ByteOrder.BIG_ENDIAN或ByteOrder.LITTLE_ENDIAN的order()方法改變ByteBuffer的字節排序方式
考慮包含下面兩個字節的ByteBuffer
在這裏插入圖片描述
如果我們以short(ByteBuffer.asShortBuffer())形式讀取數據 得到的數字是97(二進制形式爲00000000 01100001) 但是如果將ByteBuffer更改成低位優先形式 仍以short形式讀取數據 得到的數字卻是24832(二進制形式爲01100001 00000000)
這個例子展示了怎樣通過字節存放模式設置來改變字符中的字節次序
在這裏插入圖片描述

用緩衝器操縱數據
下面的圖闡明瞭nio類之間的關係 便於我們理解怎麼移動和轉換數據 例如 如果想把一個字節數組寫到文件中去 那麼就應該使用ByteBuffer.wrap()方法把字節數組包裝起來 然後用getChannel()方法在FileOutputStream上打開一個通道 接着將來自於ByteBuffer的數據寫到FileChannel中
在這裏插入圖片描述
注意 ByteBuffer是將數據移進移出通道的唯一方式 並且我們只能創建一個獨立的基本類型緩衝器 或者使用 as 方法從ByteBuffer中獲得 也就是說 我們不能把基本類型的緩衝器轉換成ByteBuffer 然而 由於我們可以經由視圖緩衝器將基本類型數據移進移出ByteBuffer 所以這也就不是什麼真正的限制了

緩衝器的細節
Buffer由數據和可以高效地訪問及操縱這些數據的四個索引組成 這四個索引是 mark(標記) position(位置) limit(界限)和capacity(容量) 下面是用於設置和復位索引以及查詢它們的值的方法
在這裏插入圖片描述
在緩衝器中插入和提取數據的方法會更新這些索引 用於反映所發生的變化
下面的示例用到一個很簡單的算法(交換相鄰字符) 以對CharBuffer中的字符進行編碼(scramble)和譯碼(unscramble)
在這裏插入圖片描述
在這裏插入圖片描述
儘管可以通過對某個char數組調用wrap()方法來直接產生一個CharBuffer 但是在本例中取而代之的是分配一個底層的ByteBuffer 產生的CharBuffer只是ByteBuffer上的一個視圖而已 這裏要強調的是 我們總是以操縱ByteBuffer爲目標 因爲它可以和通道進行交互
下面是進入symmetricScramble()方法時緩衝器的樣子
在這裏插入圖片描述
position指針指向緩衝器中的第一個元素 capacity和limit則指向最後一個元素
在程序的symmetricScramble()方法中 迭代執行while循環直到position等於limit 一旦調用緩衝器上相對的get()或put()函數 position指針就會隨之相應改變 我們也可以調用絕對的 包含一個索引參數的get()和put()方法(參數指明get()或put()的發生位置) 不過 這些方法不會改變緩衝器的position指針
當操縱到while循環時 使用mark()調用來設置mark的值 此時 緩衝器狀態如下
在這裏插入圖片描述
兩個相對的get()調用把前兩個字符保存到變量c1和c2中 調用完這兩個方法後 緩衝器如下
在這裏插入圖片描述
爲了實現交換 我們要在position=0時寫入c2 position=1時寫入c1 我們也可以使用絕對的put()方法來實現 或者使用reset()把position的值設爲mark的值
在這裏插入圖片描述
這兩個put()方法先寫c2 接着寫c1
在這裏插入圖片描述
在下一次循環迭代期間 將mark設置成position的當前值
在這裏插入圖片描述
這個過程將會持續到遍歷完整個緩衝器 在while循環的最後 position指向緩衝器的末尾 如果要打印緩衝器 只能打印出position和limit之間的字符 因此 如果想顯示緩衝器的全部內容 必須使用rewind()把position設置到緩衝器的開始位置 下面是調用rewind()之後緩衝器的狀態(mark的值則變得不明確)
在這裏插入圖片描述
當再次調用symmetricScramble()功能時 會對CharBuffer進行同樣的處理 並將其恢復到初始狀態

內存映射文件
內存映射文件允許我們創建和修改那些因爲太大而不能放入內存的文件 有了內存映射文件 我們就可以假定整個文件都放在內存中 而且可以完全把它當作非常大的數組來訪問 這種方法極大地簡化了用於修改文件的代碼 下面是一個小例子
在這裏插入圖片描述

性能
儘管 舊 的I/O流在用nio實現後性能有所提高 但是 映射文件訪問 往往可以更加顯著地加快速度 下面的程序進行了簡單的性能比較
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

文件加鎖
JDK 1.4引入了文件加鎖機制 它允許我們同步訪問某個作爲共享資源的文件 不過 競爭同一文件的兩個線程可能在不同的Java虛擬機上 或者一個是Java線程 另一個是操作系統中其他的某個本地線程 文件鎖對其他的操作系統進程是可見的 因爲Java的文件加鎖直接映射到了本地操作系統的加鎖工具
下面是一個關於文件加鎖的簡單例子
在這裏插入圖片描述

也可以使用如下方法對文件的一部分上鎖
在這裏插入圖片描述
或者
在這裏插入圖片描述
其中 加鎖的區域由size position決定 第三個參數指定是否是共享鎖

對映射文件的部分加鎖
如前所述 文件映射通常應用於極大的文件 我們可能需要對這種巨大的文件進行部分加鎖 以便其他進程可以修改文件中未被加鎖的部分 例如 數據庫就是這樣 因此多個用戶可以同時訪問到它
下面例子中有兩個線程 分別加鎖文件的不同部分
在這裏插入圖片描述

壓縮
Java I/O類庫中的類支持讀寫壓縮格式的數據流 你可以用它們對其他的I/O類進行封裝 以提供壓縮功能
這些類不是從Reader和Writer類派生而來的 而是屬於InputStream和OutputStream繼承層次結構的一部分 這樣做是因爲壓縮類庫是按字節方式而不是字符方式處理的 不過有時我們可能會被迫要混合使用兩種類型的數據流(注意我們可以使用InputStreamReader和OutputStreamWriter在兩種類型間方便地進行轉換)
在這裏插入圖片描述
儘管存在許多種壓縮算法 但是Zip和GZIP可能是最常用的 因此我們可以很容易地使用多種可讀寫這些格式的工具來操縱我們的壓縮數據

用GZIP進行簡單壓縮
GZIP接口非常簡單 因此如果我們只想對單個數據流(而不是一系列互異數據)進行壓縮 那麼它可能是比較適合的選擇 下面是對單個文件進行壓縮的例子
在這裏插入圖片描述
在這裏插入圖片描述

用Zip進行多文件保存
支持Zip格式的Java庫更加全面 利用該庫可以方便地保存多個文件 它甚至有一個獨立的類 使得讀取Zip文件更加方便 這個類庫使用的是標準Zip格式 所以能與當前那些可通過因特網下載的壓縮工具很好地協作 下面這個例子具有與前例相同的形式 但它能根據需要來處理任意多個命令行參數 另外 它顯示了用Checksum類來計算和校驗文件的校驗和的方法 一共有兩種Checksum類型 Adler32(它快一些)和CRC32(慢一些 但更準確)
在這裏插入圖片描述
在這裏插入圖片描述

Java檔案文件
Zip格式也被應用於JAR(Java ARchive Java檔案文件)文件格式中 這種文件格式就像Zip一樣 可以將一組文件壓縮到單個壓縮文件中 同Java中其他任何東西一樣 JAR文件也是跨平臺的 所以不必擔心跨平臺的問題 聲音和圖像文件可以像類文件一樣被包含在其中

Sun的JDK自帶的jar程序可根據我們的選擇自動壓縮文件 可以用命令行的形式調用它 如下所示
在這裏插入圖片描述
其中options只是一個字母集合(不必輸入任何 - 或其他任何標識符) 以下這些選項字符在Unix和Linux系統中的tar文件中也具有相同的意義 具體意義如下所示
在這裏插入圖片描述
如果壓縮到JAR文件的衆多文件中包含某個子目錄 那麼該子目錄會被自動添加到JAR文件中 且包括該子目錄的所有子目錄 路徑信息也會被保留
以下是一些調用jar的典型方法 下面的命令創建了一個名爲myJarFile.jar的JAR文件 該文件包含了當前目錄中的所有類文件 以及自動產生的清單文件
在這裏插入圖片描述
下面的命令與前例類似 但添加了一個名爲myManifestFile.mf的用戶自建清單文件
在這裏插入圖片描述
下面的命令會產生myJarFile.jar內所有文件的一個目錄表
在這裏插入圖片描述
下面的命令添加 v(詳盡)標誌 可以提供有關myJarFile.jar中的文件的更詳細的信息
在這裏插入圖片描述
假定audio classes和image是子目錄 下面的命令將所有子目錄合併到文件myApp.jar中 其中也包括了 v 標誌 當jar程序運行時 該標誌可以提供更詳細的信息
在這裏插入圖片描述
如果用0(零)選項創建一個JAR文件 那麼該文件就可放入類路徑變量(CLASSPATH)中
在這裏插入圖片描述
然後Java就可以在lib1.jar和lib2.jar中搜索目標類文件了
jar工具的功能沒有zip工具那麼強大 例如 不能夠對已有的JAR文件進行添加或更新文件的操作 只能從頭創建一個JAR文件 同時 也不能將文件移動至一個JAR文件 並在移動後將它們刪除 然而 在一種平臺上創建的JAR文件可以被在其他任何平臺上的jar工具透明地閱讀(這個問題有時會困擾zip工具)

對象序列化
Java的對象序列化將那些實現了Serializable接口的對象轉換成一個字節序列 並能夠在以後將這個字節序列完全恢復爲原來的對象 這一過程甚至可通過網絡進行 這意味着序列化機制能自動彌補不同操作系統之間的差異 也就是說 可以在運行Windows系統的計算機上創建一個對象 將其序列化 通過網絡將它發送給一臺運行Unix系統的計算機 然後在那裏準確地重新組裝 而卻不必擔心數據在不同機器上的表示會不同 也不必關心字節的順序或者其他任何細節

對象序列化特別 聰明 的一個地方是它不僅保存了對象的 全景圖 而且能追蹤對象內所包含的所有引用 並保存那些對象 接着又能對對象內包含的每個這樣的引用進行追蹤 依此類推 這種情況有時被稱爲 對象網 單個對象可與之建立連接 而且它還包含了對象的引用數組以及成員對象 如果必須保持一套自己的對象序列化機制 那麼維護那些可追蹤到所有鏈接的代碼可能會顯得非常麻煩 然而 由於Java的對象序列化似乎找不出什麼缺點 所以請儘量不要自己動手 讓它用優化的算法自動維護整個對象網 下面這個例子通過對鏈接的對象生成一個worm(蠕蟲)對序列化機制進行了測試 每個對象都與worm中的下一段鏈接 同時又與屬於不同類(Data)的對象引用數組鏈接
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

尋找類
將一個對象從它的序列化狀態中恢復出來 有哪些工作是必須的呢 舉個例子來說 假如我們將一個對象序列化 並通過網絡將其作爲文件傳送給另一臺計算機 那麼 另一臺計算機上的程序可以只利用該文件內容來還原這個對象嗎
回答這個問題的最好方法就是做一個實驗 下面這個文件位於本節的子目錄下
在這裏插入圖片描述
而用於創建和序列化一個Alien對象的文件也位於相同的目錄下
在這裏插入圖片描述
在這裏插入圖片描述
這個程序不但能捕獲和處理異常 而且將異常拋出到main()方法之外 以便通過控制檯產生報告 一旦該程序被編譯和運行 它就會在c12目錄下產生一個名爲X.file的文件 以下代碼位於一個名爲xfiles的子目錄下
在這裏插入圖片描述
打開文件和讀取mystery對象中的內容都需要Alien的Class對象 而Java虛擬機找不到Alien.class(除非它正好在類路徑Classpath內 而本例卻不在類路徑之內) 這樣就會得到一個名叫ClassNotFoundException的異常(同樣 除非能夠驗證Alien存在 否則它等於消失) 必須保證Java虛擬機能找到相關的.class文件

序列化的控制
默認的序列化機制並不難操縱 然而 如果有特殊的需要那又該怎麼辦呢 例如 也許要考慮特殊的安全問題 而且你不希望對象的某一部分被序列化 或者一個對象被還原以後 某子對象需要重新創建 從而不必將該子對象序列化
在這些特殊情況下 可通過實現Externalizable接口 代替實現Serializable接口 來對序列化過程進行控制 這個Externalizable接口繼承了Serializable接口 同時增添了兩個方法 writeExternal()和readExternal() 這兩個方法會在序列化和反序列化還原的過程中被自動調用 以便執行一些特殊操作
下面這個例子展示了Externalizable接口方法的簡單實現 注意Blip1和Blip2除了細微的差別之外 幾乎完全一致
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

下面這個例子示範瞭如何完整保存和恢復一個Externalizable對象
在這裏插入圖片描述
在這裏插入圖片描述

transient(瞬時)關鍵字
當我們對序列化進行控制時 可能某個特定子對象不想讓Java的序列化機制自動保存與恢復 如果子對象表示的是我們不希望將其序列化的敏感信息(如密碼) 通常就會面臨這種情況 即使對象中的這些信息時private(私有)屬性 一經序列化處理 人們就可以通過讀取文件或者攔截網絡傳輸的方式來訪問到它

例如 假設某個Login對象保存某個特定的登錄會話信息 登錄的合法性通過校驗之後 我們想把數據保存下來 但不包括密碼 爲做到這一點 最簡單的辦法是實現Serializable 並將password字段標誌爲transient 下面是具體的代碼
在這裏插入圖片描述
在這裏插入圖片描述

Externalizable的替代方法
如果不是特別堅持實現Externalizable接口 那麼還有另一種方法 我們可以實現Serializable接口 並添加(注意是 添加 而非 覆蓋 或者 實現)名爲writeObject()和readObject()的方法 這樣一旦對象被序列化或者被反序列化還原 就會自動地分別調用這兩個方法 也就是說 只要我們提供了這兩個方法 就會使用它們而不是默認的序列化機制
這些方法必須具有準確的方法特徵簽名
在這裏插入圖片描述

還有另外一個技巧 在你的writeObject()內部 可以調用defaultWriteObject()來選擇執行默認的writeObject() 類似地 在readObject()內部 我們可以調用defaultReadObject() 下面這個簡單的例子演示瞭如何對一個Serializable對象的存儲與恢復進行控制
在這裏插入圖片描述

版本控制
有時可能想要改變可序列化類的版本(比如源類的對象可能保存在數據庫中) 雖然Java支持這種做法 但是你可能只在特殊的情況下才這樣做 此外 還需要對它有相當深程度的瞭解 從http://java.sun.com處下載的JDK文檔中對這一主題進行了非常徹底的論述
我們會發現在JDK文檔中有許多註解是從下面的文字開始的
警告 該類的序列化對象和未來的Swing版本不兼容 當前對序列化的支持只適用於短期存儲或應用之間的RMI
這是因爲Java的版本控制機制過於簡單 因而不能在任何場合都可靠運轉 尤其是對JavaBeans更是如此 有關人員正在設法修正這一設計 也就是警告中的相關部分

使用 持久性
一個比較誘人的使用序列化技術的想法是 存儲程序的一些狀態 以便我們隨後可以很容易地將程序恢復到當前狀態 但是在我們能夠這樣做之前 必須回答幾個問題 如果我們將兩個對象 它們都具有指向第三個對象的引用 進行序列化 會發生什麼情況 當我們從它們的序列化狀態恢復這兩個對象時 第三個對象會只出現一次嗎 如果將這兩個對象序列化成獨立的文件 然後在代碼的不同部分對它們進行反序列化還原 又會怎樣呢
下面這個例子說明了上述問題
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

如果我們想保存系統狀態 最安全的做法是將其作爲 原子 操作進行序列化 如果我們序列化了某些東西 再去做其他一些工作 再來序列化更多的東西 如此等等 那麼將無法安全地保存系統狀態 取而代之的是 將構成系統狀態的所有對象都置入單一容器內 並在一個操作中將該容器直接寫出 然後同樣只需一次方法調用 即可以將其恢復
下面這個例子是一個想象的計算機輔助設計(CAD)系統 該例演示了這一方法 此外 它還引入了static字段的問題 如果我們查看JDK文檔 就會發現Class是Serializable的 因此只需直接對Class對象序列化 就可以很容易地保存static字段 在任何情況下 這都是一種明智的做法
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

恢復對象相當直觀
在這裏插入圖片描述
可以看到 xPos yPos以及dim的值都被成功地保存和恢復了 但是對static信息的讀取卻出現了問題 所有讀回的顏色應該都是 3 但是真實情況卻並非如此 Circle的值爲1(定義爲RED) 而Sequare的值爲0(記住 它們是在構造器中初始化的) 看上去似乎static數據根本沒有被序列化 確實如此 儘管Class類是Serializable的 但它卻不能按我們所期望的方式運行 所以假如想序列化static值 必須自己動手去實現
這正是Line中的serializeStaticState()和deserializeStaticState()兩個static方法的用途 可以看到 它們是作爲存儲和讀取過程的一部分被顯式地調用的(注意必須維護寫入序列化文件和從該文件中讀回的順序) 因此 爲了使CADState.java正確運轉起來 我們必須

  1. 爲幾何形狀添加serializeStaticState()和deserializeStaticState()
  2. 移除ArrayList shapeTypes以及與之有關的所有代碼
  3. 在幾何形狀內添加對新的序列化和反序列化還原靜態方法的調用
    另一個要注意的問題是安全 因爲序列化也會將private數據保存下來 如果你關心安全問題 那麼應將其標記成transient 但是這之後 還必須設計一種安全的保存信息的方法 以便在執行恢復時可以復位那些private變量

XML
對象序列化的一個重要限制是它只是Java的解決方案 只有Java程序才能反序列化這種對象 一種更具互操作性的解決方案是將數據轉換爲XML格式 這可以使其被各種各樣的平臺和語言使用

作爲一個示例 假設有一個Person對象 它包含姓和名 你想將它們序列化到XML中 下面的Person類有一個getXML()方法 它使用XOM來產生被轉換爲XML的Element對象的Person數據 還有一個構造器 接受Element並從中抽取恰當的Person數據
在這裏插入圖片描述
在這裏插入圖片描述

從XML文件中反序列化Person對象也很簡單
在這裏插入圖片描述

Preferences
Preferences API與對象序列化相比 前者與對象持久性更密切 因爲它可以自動存儲和讀取信息 不過 它只能用於小的 受限的數據集合 我們只能存儲基本類型和字符串 並且每個字符串的存儲長度不能超過8K(不是很小 但我們也並不想用它來創建任何重要的東西) 顧名思義 Preferences API用於存儲和讀取用戶的偏好(preferences)以及程序配置項的設置
Preferences是一個鍵 值集合(類似映射) 存儲在一個節點層次結構中 儘管節點層次結構可用來創建更爲複雜的結構 但通常是創建以你的類名命名的單一節點 然後將信息存儲於其中 下面是一個簡單的例子
在這裏插入圖片描述
這裏用的是userNodeForPackage() 但我們也可以選擇用systemNodeForPackage() 雖然可以任意選擇 但最好將 user 用於個別用戶的偏好 將 system 用於通用的安裝配置 因爲main()是靜態的 因此PreferencesDemo.class可以用來標識節點 但是在非靜態方法內部 我們通常使用getClass() 儘管我們不一定非要把當前的類作爲節點標識符 但這仍不失爲一種很有用的方法
一旦我們創建了節點 就可以用它來加載或者讀取數據了 在這個例子中 向節點載入了各種不同類型的數據項 然後獲取其keys() 它們是以String[]的形式返回的 如果你習慣於keys()屬於集合類庫 那麼這個返回結果可能並不是你所期望的 注意get()的第二個參數 如果某個關鍵字下沒有任何條目 那麼這個參數就是所產生的默認值 當在一個關鍵字集合內迭代時 我們總要確信條目是存在的 因此用null作爲默認值是安全的 但是通常我們會獲得一個具名的關鍵字 就像下面這條語句
在這裏插入圖片描述
在通常情況下 我們希望提供一個合理的默認值 實際上 典型的習慣用法可見下面幾行
在這裏插入圖片描述
這樣 在我們第一次運行程序時 UsageCount的值是0 但在隨後引用中 它將會是非零值
在我們運行PreferencesDemo.java時 會發現每次運行程序時 UsageCount的值都會增加1 但是數據存儲到哪裏了呢 在程序第一次運行之後 並沒有出現任何本地文件 Preferences API利用合適的系統資源完成了這個任務 並且這些資源會隨操作系統不同而不同 例如在Windows裏 就使用註冊表(因爲它已經有 鍵值對 這樣的節點對層次結構了) 但是最重要的一點是 它已經神奇般地爲我們存儲了信息 所以我們不必擔心不同的操作系統是怎麼運作的
還有更多的Preferences API 參閱JDK文檔可很容易地理解更深的細節

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章