天天看點

Java I/O Java I/O

标簽 : Java基礎

Java的I/O功能通過<code>java.io</code>包下的類和接口來支援,在<code>java.io</code>包下主要包括<code>輸入/輸出</code>兩種IO流,每種<code>輸入/輸出流</code>又可分為<code>位元組流</code>和<code>字元流</code>兩大類.位元組流支援以位元組(8位)為機關的IO操作,而字元流則以字元(16位-Java中)為機關進行IO操作. 除此之外,Java的IO流還使用裝飾者模式,将IO流分成底層<code>節點流</code>和上層<code>處理流</code>,節點流直接和底層的實體存儲節點關聯,雖然不同的實體節點擷取的節點流可能存在差異,但程式可以把不同的實體節點包裝成統一的處理流,進而允許程式使用統一的IO代碼來操作不同的實體節點.
Java使用<code>java.io.File</code>來提供對底層檔案的抽象, <code>File</code>能夠<code>建立/删除/重命名</code>檔案和目錄,但不能通路檔案内容本身(需要使用IO流).

<code>File</code>類提供了很多實用的方法,我們可以将其分為以下幾類:

檔案名相關方法

<code>getAbsoluteFile()</code> <code>getAbsolutePath()</code> <code>getName()</code> <code>getParent()</code> <code>getParentFile()</code> <code>getPath()</code> <code>renameTo(File dest)</code>

檔案狀态相關方法

<code>exists()</code> <code>canExecute()</code> <code>canRead()</code> <code>canWrite()</code> <code>isFile()</code> <code>isDirectory()</code> <code>isAbsolute()</code>(UNIX/Linux中是否以<code>/</code>開頭) <code>isHidden()</code> <code>lastModified()</code> <code>length()</code>

檔案操作

<code>createNewFile()</code> <code>createTempFile(String prefix, String suffix)</code> <code>delete()</code> <code>deleteOnExit()</code> <code>setExecutable(boolean executable)</code> <code>setReadOnly()</code>

目錄操作

如: <code>mkdir()</code> <code>mkdirs()</code> <code>list()</code> <code>list(FilenameFilter filter)</code> <code>listFiles()</code> <code>listFiles(FileFilter filter)</code> <code>listRoots()</code>

<code>InputStream</code>與<code>Reader</code>是所有輸入流的抽象基類, 雖然本身不能建立執行個體來執行輸入, 但作為所有輸入流的模闆, 他們的方法是所有輸入流都通用的.

<code>InputStream</code>包含如下三個方法:

<code>int read()</code>

<code>int read(byte[] b)</code>

<code>int read(byte[] b, int off, int len)</code>

<code>Reader</code>包含如下三個方法:

<code>int read(char[] cbuf)</code>

<code>int read(char[] cbuf, int off, int len)</code>

對比<code>InputStream</code>和<code>Reader</code>所提供的方法, 這兩個類的功能基本相同, 隻是一個提供了位元組<code>byte</code>讀, 一個提供了字元<code>char</code>讀.

除此之外, <code>InputStream</code>和<code>Reader</code>還支援幾個方法來移動記錄指針以實作跳讀, 重複讀等操作.

方法

釋義

<code>void mark(int readlimit)</code>

Marks the current position in this input stream.

<code>boolean markSupported()</code>

Tests if this input stream supports the mark and reset methods.

<code>void reset()</code>

Repositions this stream to the position at the time the mark method was last called on this input stream.

<code>long skip(long n)</code>

Skips over and discards n bytes of data from this input stream.

兩個流都提供了如下三個方法:

<code>void write(byte[]/char[] b)</code>

<code>void write(byte[]/char[] b, int off, int len)</code>

<code>void write(int b)</code>

因為字元流直接以字元作為操作機關, 是以<code>Writer</code>可以用字元串來代替字元數組(以<code>String</code>對象作為參數). <code>Writer</code>還包含如下方法:

<code>void write(String str)</code>

<code>void write(String str, int off, int len)</code>

<code>Writer append(char c)</code>

<code>Writer append(CharSequence csq)</code>

<code>Writer append(CharSequence csq, int start, int end)</code>

小結:

使用Java IO流執行完IO後,不能忘記關閉流,關閉流才可以保證實體資源會被回收(關閉輸出流還可以保證将緩沖區中的資料<code>flush</code>到實體節點中,因為在執行<code>close()</code>方法之前,會自動執行輸出流的<code>flush()</code>方法).

Java 1.7改寫了所有的IO資源類,他們都實作了<code>AutoCloseable</code>接口,是以都可通過自動關閉的<code>try</code>語句來自動關閉這些流.

通常來說,位元組流的功能比字元流的功能強大,因為計算機裡所有的資料都是二進制的,是以位元組流可以處理所有所有的二進制檔案.

如果使用位元組流來處理文本檔案,則需要将這些位元組轉換成字元,增加了程式設計的複雜度.是以有一個規則:如果進行IO是文本内容,則應該考慮使用字元流;如果進行IO的是二進制内容, 則應該考慮使用位元組流.

節點流的的構造參數是實體IO節點,而處理流的構造參數是已經存在的流.

常用節點流:

\

InputStream

OutputStream

Reader

Writer

檔案

FileInputStream

FileOutputStrean

FileReader

FileWriter

數組

ByteArrayInputStream

ByteArrayOutputStream

CharArrayReader

CharArrayWriter

字元串

*

StringReader

StringWriter

管道

PipedInputStream

PipedOutputStream

PipedReader

PipedWriter

常用處理流:

緩沖流

BufferedInputStrean

BufferedOutputStream

BufferedReader

BufferedWriter

轉換流

InputStreamReader

OutputStreamWriter

資料流

DataInputStream

DataOutputStream

對象流

ObjectInputStream

ObjectOutStream

合并流

SequenceInputStream

回退流

PushbackInputStream

PushbackReader

列印流

PrintStream

PrintWriter

處理流與節點流相比可以隐藏底層節點流的差異,對外提供更加統一的IO方法,讓開發人員隻需關心進階流的操作(使用更加簡單, 執行效率更高).

使用處理流的思路:使用處理流來包裝節點流,程式通過處理流執行IO,讓節點流與底層IO裝置/檔案互動.

在使用處理流包裝了節點流之後, 關閉輸入/輸出流資源時, 隻要關閉最上層的處理流即可.關閉最上層的處理流時, 系統會自動關閉該處理流包裝的節點流.

上表中列出了一種以數組為實體節點的節點流,位元組流以位元組數組為節點<code>ByteArrayInputStream</code>/<code>ByteArrayOutputStream</code>,字元流以字元數組為節點<code>CharArrayReader</code>/<code>CharArrayWriter</code>, 這種數組流除了在建立節點流需要傳入數組外,用法上與其他節點流完全類似.數組流類似還可以使用字元串作為實體節點,用于實作從字元串中讀取字元串(String)<code>java.io.StringReader</code>, 或将内容寫入字元串(StringBuffer)<code>java.io.StringWriter</code>.

<code>BufferedInputStream</code> <code>BufferedOutputStream</code> <code>BufferedReader</code>

<code>BufferedWriter</code> 四個緩沖流增加了緩沖功能, 可以提高IO效率, 但是需要使用<code>flush()</code>才可以将緩沖區的内容寫入實際的實體節點.

Java I/O流體系提供了兩個轉換流, 用于實作将位元組流轉換成字元流:

<code>java.io.InputStreamReader</code>将位元組輸入流轉換成字元輸入流

<code>java.io.OutputStreamWriter</code>将位元組輸出流轉換成字元輸出流

在IO流體系中, 有兩個特殊的流與衆不同: <code>PushbackInputStream</code> <code>PushbackReader</code> 他們都提供了如下三個方法:

<code>void unread(byte[]/char[] buf)</code>

Pushes back an array of bytes/characters by copying it to the front of the pushback buffer.

<code>void unread(byte[]/char[] buf, int off, int len)</code>

Pushes back a portion of an array of bytes/characters by copying it to the front of the pushback buffer.

<code>void unread(int b/c)</code>

Pushes back a single byte/character by copying it to the front of the pushback buffer.

這兩個推回輸入流都帶有一個推回緩沖區, 當程式調用者兩個的<code>unread()</code>方法時,系統将會把指定的資料推回到該緩沖區中, 而這兩個流在每次調用<code>read()</code>方法時,都會先從推回緩沖區讀取,隻有完全讀取了推回緩沖區的内容後, 才會到原輸入流中擷取.

這樣, 當程式建立該流時, 需要指定推回緩沖區大小(預設為1),如果在程式中推回的資料超出了推回緩沖區的大小, 則會抛出<code>java.io.IOException: Pushback buffer overflow</code>.

<code>java.io.RandomAccessFile</code>與普通的Java I/O流不同的是, 他可以支援<code>随機通路</code>檔案, 程式可以直接跳轉到檔案的任意地方讀寫資料(是以如果隻是通路檔案的部分内容,或向已存在的檔案追加資料, <code>RandomAccessFile</code>是更好的選擇).但<code>RandomAccessFile</code>也有一個局限就是隻能讀寫檔案, 不能讀寫其他IO節點.

<code>RandomAccessFile</code>對象包含了一個記錄指針, 用以辨別目前讀寫位置, 當程式建立一個<code>RandomAccessFile</code>對象時, 記錄指針位于檔案頭, 當讀/寫n個位元組後, 指針将會向後移動n個位元組.除此之外<code>RandomAccessFile</code>還可以(前/後)自由移動該記錄指針,<code>RandomAccessFile</code>提供了如下方法來操作檔案記錄指針:

<code>long getFilePointer()</code>

Returns the current offset in this file.

<code>void seek(long pos)</code>

Sets the file-pointer offset, measured from the beginning of this file, at which the next read or write occurs.

<code>RandomAccessFile</code>既可以讀檔案, 也可以寫檔案, 是以他提供了完全類似于<code>InputStream</code>的<code>read()</code>方法, 也提供了完全類似于<code>OutputStream</code>的<code>write()</code>方法, 此外他還包含了一系列的<code>readXxx()</code> <code>writeXxx()</code>來完成IO(其實這幾個操作是<code>DataInput</code> <code>DataOutput</code>接口來提供的, <code>DataInputStream</code>/<code>DataOutputStream</code> <code>ObjectInputStream</code>/<code>ObjectOutputStream</code>也實作了這兩個接口).

在構造一個<code>RandomAccessFile</code>時, 都需要提供一個<code>String mode</code>參數, 用于指定<code>RandomAccessFile</code>的通路模式, 該參數有如下取值:

mode

模式

<code>"r"</code>

隻讀

<code>"rw"</code>

讀寫, 如果檔案不存在, 則建立

<code>"rws"</code>

讀寫,相對于”rw”,還要求對<code>檔案内容</code>或<code>檔案中繼資料</code>的更新都同步寫入底層存儲資料

<code>"rwd"</code>

讀寫,相對于”rw”,<code>隻</code>要求對<code>檔案内容</code>的更新同步寫入底層存儲資料

注意: <code>RandomAccessFile</code>不能直接向檔案的指定位置插入資料,不然新插入的資料會覆寫檔案的原内容.如果需要向指定的位置插入資料,程式需要先把指定插入點後的内容讀入緩沖區, 等把需要插入的資料寫入完成後, 再将緩沖區的内容追加到檔案後面.
序列化機制使得對象可以脫離程式的運作環境而獨立存在: 對象序列化機制允許把記憶體中的Java對象轉換成平台無關的二進制流, 進而可以把這種二進制流持久的儲存在磁盤上, 或通過網絡将這種二進制流傳輸到另一個網絡節點. 其他程式一旦擷取到了這個二進制流, 都可以将他恢複成原先的Java對象.

如果需要讓某個對象支援序列化機制, 則必須讓他的類是可序列化的(serializable). 該類必須實作如下接口之一:

<code>java.io.Serializable</code>

<code>java.io.Externalizable</code>

注意: 對象的類名, 執行個體變量(基本類型/數組/引用對象)都會被序列化; 而方法/類變量(static)/transient執行個體變量都不會序列化.

使用<code>Serialiable</code>實作對象序列化隻需實作這個接口即可(而這個接口隻是一個<code>Tag</code>接口).

注意:

反序列化讀取的僅僅是Java<code>對象</code>的資料, 而不是Java類, 是以采用反序列化恢複Java對象時,必須提供對象所屬的類的class檔案, 否則将會引發<code>ClassNotFoundException</code>.

從上例看到: <code>Bean</code>類并沒有提供無參構造器, 是以可以證明<code>反序列化機制無須通過構造器來初始化對象</code>.

如果使用序列化機制向檔案中寫入了多個Java對象, 反序列化時必須按實際寫入的順序讀取.

根據經驗: 像<code>Date</code> <code>BigInteger</code>這樣的值類應該實作<code>Serializable</code>接口,大多數的集合也應該如此. 但代表活動實體的類, 如線程池(Thread Pool), 一般不應該實作<code>Serializable</code>.

如果某個類的成員變量不是基本類型或String類型,而是另一個引用類型, 那麼這個引用類必須是可序列化的, 否則擁有該類型成員變量的類也是不可序列化的.

Java序列化算法

所有儲存到磁盤(或傳輸到網絡中)的對象都有一個序列化編号.

當程式試圖序列化一個對象時, 程式将先檢查該對象是否已經被序列化過, 隻有該對象(在本次虛拟機的上下文<code>Context</code>中)從未被序列化過, 系統才會将該對象轉換成位元組序列并輸出.

如果某個對象已經序列化過(即使該對象的執行個體變量後來發生了改變), 程式将不再重新序列化該對象.

Java序列化機制允許為序列化的類提供一個<code>private static final long serialVersionUID = xxxL;</code>值, 該值用于辨別該Java類的序列化版本; 一個類更新之後, 隻要他的<code>serialVersionUID</code>值不變, 序列化機制也會把它們當成同一個序列化版本(由于提供了<code>serialVersionUID</code>之後JVM不需要再次計算該值,是以還有個小小的性能好處).

可以通過JDK提供的serialver工具來提取類的<code>serialVersionUID</code>值.

如果不顯式定義<code>serialVersionUID</code>的值,可能會造成以下問題:

該值将由JVM根據類的相關資訊計算,而修改後的類的計算結果與修改前的類的計算結果往往不同,進而造成對象的反序列化因為類的版本不相容而失敗.

不利于程式在不同JVM之間移植, 因為不同的編譯器對該變量的計算政策可能不同, 而從造成類雖然沒有改變, 但因為JVM的不同, 也會出現序列化版本不相容而導緻無法正确反序列化的現象.

在一些的場景下, 如果一個類裡面包含的某些變量不希望對其序列化(或某個執行個體變量是不可序列化的,是以不希望對該執行個體變量進行<code>遞歸序列化</code>,以避免引發<code>java.io.NotSerializableException</code>異常); 或者将來這個類的實作可能改動(最大程度的保持版本相容), 或者我們需要自定義序列化規則, 在這種情景下我們可選擇實用自定義序列化.

通過在執行個體變量前面加<code>transient</code>關鍵字以指定Java序列化時無需理會該執行個體變量(注意: <code>transient</code>關鍵字隻能用于修飾執行個體變量, 不可修飾Java其他成分).

被<code>transient</code>修飾的執行個體變量被稱為瞬時變量.

使用<code>transient</code>關鍵字修飾執行個體變量雖然簡單, 但被<code>transient</code>修飾的執行個體變量将完全被隔離在序列化機制之外, 這樣導緻反序列化恢複Java對象時無法取得該執行個體變量的值. 是以, Java還提供了另一種自定義序列化機制,通過提供<code>writeObject()</code> <code>readObject()</code> <code>readObjectNoData()</code>等方法可以讓程式控制如何序列化各執行個體變量, 甚至完全不序列化某些執行個體變量.

<code>private void writeObject(java.io.ObjectOutputStream output) throws IOException;</code>

負責序列化類執行個體,以便<code>readObject()</code>可以恢複它;通過重寫該方法, 可以實作完全控制該類的序列化機制,自主決定哪些執行個體變量需要執行個體化,需要怎樣序列化. 在預設情況下, 該方法會調用<code>output.defaultWriteObject()</code>來儲存Java對象的各執行個體變量, 進而可以實作序列化對象的目的.

<code>private void readObject(java.io.ObjectInputStream input) throws IOException, ClassNotFoundException;</code>

負責反序列化類執行個體,通過重寫該方法,可以完全控制該類的反序列化機制,自主決定需要反序列化哪些執行個體變量,以及如何進行反序列化.在預設情況下, 該方法會調用<code>input.defaultReadObject()</code>來恢複Java對象的非瞬時變量.

一般<code>readObject()</code>應與<code>writeObject()</code>方法對應,如果<code>writeObject()</code>對Java對象的執行個體變量進行了一些處理, 則應該在<code>readObject()</code>方法中對其執行個體變量進行相應的反處理,以便正确恢複該對象.

由于我們可以自己定制序列化規則, 是以, 在網絡傳輸中, 可以對對象執行個體進行加密, 在讀出時自動解密, 這樣即使在傳輸過程被截獲, 擷取到的也是加密後的值.但<code>writeObject()</code>方法的加密規則必須與<code>readObject()</code>的解密規則一緻.

建議<code>readObject()</code> <code>writeObject()</code>的方法内首先調用<code>defaultReadObject()</code> <code>defaultWriteObject()</code>;

<code>private void readObjectNoData() throws ObjectStreamException;</code>

當序列化流不完整時, <code>readObjectNoData()</code>方法可以用來正确地反序列化對象.例如, 接收方使用的反序列化類的版本不同于發送方,或接收方版本擴充的類不是發送方版本擴充的類時,系統都會調用<code>readObjectNoData()</code>方法來初始化反序列化的對象.

<code>private Object writeReplace() throws ObjectStreamException;</code>

Java序列化機制保證在序列化某個對象之前, 先調用對象的<code>writeReplace()</code>方法, 如果該方法傳回另一個Java對象, 則系統轉化為序列化另一個對象.

與<code>writeReplace()</code>對應, 序列化還有一個<code>readResolve()</code>方法

<code>private Object readResolve() throws ObjectStreamException;</code>

可以實作保護性複制整個對象, 這個方法會緊接着<code>readObject()</code>之後調用, 該方法的傳回值将會替換原來反序列化的對象, 而原來<code>readObject()</code>反序列化的對象将會被立即丢棄.

<code>readResolve()</code>與<code>writeReplace()</code>還可以使用其他的通路修飾符, 但建議使用<code>private</code>修飾.

實作<code>Externalizable</code>接口以實作對象序列化, 這種序列化方式完全由程式員決定存儲和恢複對象資料的機制.該接口提供了如下兩個方法:

<code>public void writeExternal(ObjectOutput out) throws IOException;</code>

序列化

<code>public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;</code>

反序列化

使用<code>Runtime</code>對象的<code>exec()</code>方法可以運作作業系統平台上的其他程式, 該方法傳回一個<code>Process</code>對象, 代表由該Java程式啟動的子程序.<code>Process</code>提供如下方法, 用于主程序和子程序進行通信:

<code>InputStream getErrorStream()</code>

Returns the input stream connected to the error output of the subprocess.

<code>InputStream getInputStream()</code>

Returns the input stream connected to the normal output of the subprocess.

<code>OutputStream getOutputStream()</code>

Returns the output stream connected to the normal input of the subprocess.

注意: Input/Output是站在主程序角度來看的.

注意: 上面程式使用到了Guava的<code>CharStreams</code>, 其詳細用法請參考我的下一篇部落格<code>Java I/O 擴充</code>.暫時可在pom中添加如下依賴: