六、Typical uses of I/O streams
/* * @filename IOStreamDemo.java * @author Bruce Eckel * * ToDo Typical I/O stream configurations. */ import java.io.EOFException; import java.io.IOException; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.RandomAccessFile; import java.io.StringReader; public class IOStreamDemo { // Throw exceptions to console: public static void main(String[] args) throws IOException { String s = null; String s2 = null; // 1. Reading input by lines: BufferedReader in = new BufferedReader( new FileReader("IOStreamDemo.java")); while((s = in.readLine()) != null) s2 += s + "\n"; in.close(); // 1b. Reading standard input: BufferedReader stdin = new BufferedReader( new InputStreamReader(System.in)); System.out.print("Enter a line:"); System.out.println(stdin.readLine()); // 2. Input from memory StringReader in2 = new StringReader(s2); int c; while((c = in2.read()) != -1) System.out.print((char) c); // 3. Formatted memory input try { DataInputStream in3 = new DataInputStream( new ByteArrayInputStream(s2.getBytes())); while(true) System.out.print((char) in3.readByte()); } catch(EOFException e) { System.err.println("End of Stream"); } // 4. File output try { BufferedReader in4 = new BufferedReader( new StringReader(s2)); PrintWriter out1 = new PrintWriter( new BufferedWriter(new FileWriter("IODemo.out"))); int lineCount = 1; while((s = in4.readLine()) != null) out1.println(lineCount++ + ": " + s); out1.close(); } catch(EOFException e) { System.err.println("End of Stream"); } // 5. Storing & recovering data try { DataOutputStream out2 = new DataOutputStream( new BufferedOutputStream(new FileOutputStream("Data.txt"))); out2.writeDouble(3.14159); out2.writeChars("That was pi\n"); out2.writeBytes("That was pi\n"); out2.close(); DataInputStream in5 = new DataInputStream( new BufferedInputStream(new FileInputStream("Data.txt"))); BufferedReader in5br = new BufferedReader( new InputStreamReader(in5)); // Must use DataInputStream for data System.out.println(in5.readDouble()); // Can now use the "proper" readLine() System.out.println(in5br.readLine()); // But the line comes out funny // The one created with writeBytes is OK: System.out.println(in5br.readLine()); } catch(EOFException e) { System.err.println("End of Stream"); } // 6. Reading / Writing random access files RandomAccessFile rf = new RandomAccessFile("rtest.dat", "rw"); for(int i = 0; i < 10; i++) rf.writeDouble(i * 1.414); rf.close(); rf = new RandomAccessFile("rtest.dat", "rw"); rf.seek(5 * 8); rf.writeDouble(47.0001); rf.close(); rf = new RandomAccessFile("rtest.dat", "r"); for(int i = 0; i < 10; i++) System.out.println("Value " + i + ": " + rf.readDouble()); rf.close(); } }
(1)Input streams
- Buffered input file
- 需要開啟一個文件用於字符的讀取時,可以使用 FileReader 並以一個 String 或 File 對象指定文件名。基於速度考量,通常會把這個 FileReader 的 reference 傳給 BufferedReader 的 constructor 使這個文件具備緩衝功能。
- BufferedReader 的 readLine() 函數會將換行符過濾掉,所以使用它的時候必須自行添加換行符。
- java.lang.System 的三個 I/O 成員(final static):
- err:PrintStream
- out:InputStream
- in:PrintStream
- Input from memory
- 通過 String(已含內容)產生 StringReader。
- java.io.Reader(abstract class)的 read() 函數返回的是 int,所以使用 read() 時必須根據實際進行轉型。
- Formatted memory input
- 讀取格式化數據通常使用的是 DataInputStream(byte-oriented),因此也就只能使用 InputStream classes 而不能使用 Reader classes。
- 任何一個 Byte 對 DataInputStream 的 readByte() 函數來說都是合法的結果,所以不能憑它的返回值來判斷輸入是否結束。
- DataInputStream 的 available()(繼承自 java.io.FilterInputStream)函數返回可供讀取的字符數。但它的運作方式取決於所輸入的對象,所以應當結合實際使用。
- File output
- 產生一個 FileWriter 對象連接至文件,將其 reference 傳給 BufferedWriter 的 constructor(緩衝可以大幅提高 I/O 效能)。如果需要格式化輸出,可以再將 BufferedWriter 的 object reference 傳給 PrintWriter 的 constructor。(這樣產生的文件是文本文件)
- 使用 Buffered 類的 classes 緩衝輸出文件後,必須調用 close() 函數關閉文件,否則緩衝區的內容可能不會被清空從而得不到完整的結果。
(2)Output streams
- Storing and recovering data
- output streams 主要分為兩種:1)為了讓人直接查看結果(如 java.io.PrintWriter);2)為了讓 DataInputStream 可以再次讀取(Random Access File 不屬于上述的兩種,但其數據格式與 DataInputStream 和 DataOutputStream 相容)。
- DataOutputStream 使用 writeChars() 和 writeBytes() 這兩個函數輸出字符串。其中 writeChars() 是以 16bit 的 Unicode 字符進行輸出的,所以如果用 readLine() 讀取這些文本,那麽每個字符間都會多了一個空格,這是因為 Unicode 會額外插入一個 byte。因此,對 ASCII 而言,使用 writeBytes() 加上換行符輸出,然後以 readLine() 讀取的方法會更為簡單。
- Random access files
- java.io.RandomAccessFile 完全獨立於 I/O 繼承體系之外。由於它實現了 DataInput 和 DataOutput 接口,所以它無法和 InputStream / OutputStream 的子類搭配使用(例如,無法為其加上緩衝功能)。
(3)A bug?
- 數據的寫入必須出現在文本之前,否則在讀取的時候就會擲出 EOFException。
/* * @filename IOProblem.java * @author Bruce Eckel * * ToDO Java 1.1 and higher I/O Problem Deom. */ import java.io.IOException; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStreamReader; public class IOProblem { // Throw exceptions to console: public static void main(String[] args) throws IOException { DataOutputStream out = new DataOutputStream( new BufferedOutputStream(new FileOutputStream("Data.txt"))); out.writeDouble(3.14159); out.writeBytes("That was the value of pi\n"); out.writeBytes("This is pi/2:\n"); out.writeDouble(3.14159/2); out.close(); DataInputStream in = new DataInputStream( new BufferedInputStream(new FileInputStream("Data.txt"))); BufferedReader inbr = new BufferedReader( new InputStreamReader(in)); // The doubles written BEFORE the line of text // read back correctly: System.out.println(in.readDouble()); // Read the lines of text: System.out.println(inbr.readLine()); System.out.println(inbr.readLine()); // Trying to read the doubles after the line // produces an end-of-file exception: System.out.println(in.readDouble()); } }
3.14159 That was the value of pi This is pi/2: Exception in thread "main" java.io.EOFException at java.io.DataInputStream.readFully(Unknown Source) at java.io.DataInputStream.readLong(Unknown Source) at java.io.DataInputStream.readDouble(Unknown Source) at IOProblem.main(IOProblem.java:42)
(4)Piped streams
- PipedInputStream / PipedOutputStream / PipedReader / PipedWriter 主要用於 multithreads。
七、Standard I/O
- Standard I/O:可為程序所用的單一信息流。所有的程序輸入皆可取自 standard input,所有的輸出皆可送至 standard output;所有的錯誤信息皆可送至 standard error。
(1)Reading from standard input
- System.out 和 System.err 都被包裝為 PrintStream object(in 和 err 是 java.lang.System 的 final static PrintStream 成員),可以直接使用;而 System.in 則是原始的 InputStream,所以在讀取前必須加以包裝(通常是 InputStreamReader + BufferedReader)。
/* * @filename Echo.java * @author Bruce Eckel * * ToDo How to read from standard input. */ import java.io.IOException; import java.io.BufferedReader; import java.io.InputStreamReader; public class Echo { public static void main(String[] args) throws IOException { String s; BufferedReader in = new BufferedReader( new InputStreamReader(System.in)); while((s = in readLine()).length() != 0) System.out.println(s); // An empty line will terminate the program. } }
(2)Changing System.out to PrintWriter
- System.out 是 PrintStream,而 PrintStream 則是一個 OutputStream。
- PrintWriter 有一個構造函數可以以 OutputStream 為參數,將 System.out 轉換為 PrintWriter。
/* * @filename ChangeSystemOut.java * @author Bruce Eckel * * ToDo Turn System.out into a PrintWriter. */ import java.io.PrintWriter; public class ChangeSystemOut { public static void main(String[] args) { /* 此處不能使用 PrintWriter(OutputStream out) 而必須使用 PrintWriter(OutputStream out, boolean autoFlush) 並將 autoFlush 設置為 true 將自動清空緩衝的功能開啟 否則很可能會看不到輸出。 */ PrintWriter out = new PrintWriter(System.out, true); out.println("Hello, world"); } }
(3)Redirecting standard I/O
- java.io.System 提供了幾個 static 函數,可對 standard input、standard output、standard error 等 I/O streams 進行重定向:
- setIn(InputStream)
- setOut(PrintStream)
- setErr(PrintStream)
- I/O 重定向處理的是以 byte 而非 character 為單位的 streams,因此使用的是 InputStream 和 OutputStream 而不是 Reader 和 Writer。
/* * @filename Redirecting.java * @author Bruce Eckel * * ToDo Demostrates standard I/O redirection. */ import java.io.IOException; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStreamReader; import java.io.PrintStream; public class Redirecting { public static void main(String[] args) throws IOException { BufferedInputStream in = new BufferedInputStream( new FileInputStream("Redirecting.java")); PrintStream out = new PrintStream( new BufferedOutputStream(new FileOutputStream("test.out"))); System.setIn(in); System.setOut(out); System.setErr(out); BufferedReader br = new BufferedReader( new InputStreamReader(System.in)); String s; while((s = br.readLine()).length() != 0) System.out.println(s); out.close(); // Remember this!!! } }
八、Compression
- Java I/O library 提供了一系列的 classes 讓我們可以以壓縮格式對 streams 進行讀寫,這些 classes 將既有的 I/O classes 包裝起來並提供壓縮功能。
- Java I/O 的 compression library 全部繼承自 InputStream / OutputStream,這是因為它們處理的是 bytes 而不是 characters。
compression class | function |
java.util.zip.CheckedInputStream | getCheckSum() 能針對任意類型的 InputStream 產生 checksum(校驗碼)而不僅是解壓 |
java.util.zip.CheckedOutputStream | getCheckSum() 能針對任意類型的 OutputStream 產生 checksum(校驗碼)而不僅是壓縮 |
java.util.zip.DeflaterOutputStream | compression classes 的 base class |
java.util.zip.ZipOutputStream | 可將數據壓縮為 Zip 格式 |
java.util.zip.GZIPOutputStream | 可將數據壓縮為 GZIP 格式 |
java.util.zip.InflaterInputStream | decompression classes 的 base class |
java.util.zip.ZipInputStream | 可將以 Zip 格式存儲的數據解壓 |
java.util.zip.GZIPInputStream | 可將以 GZIP 格式存儲的數據解壓 |
(1)Simple compression with GZIP
- compression 相關 classes 的運用方式很直觀:只需將 output stream 包裝成 GZIPOutputStream / ZipOutputStream,並將 input stream 包裝成 GZIPInputStream / ZipInputStream,然後直接進行一般的 I/O 處理即可。
- GZIP 接口較為簡單因此比較適合對單一的 stream 數據而非多份相異數據進行壓縮。
/* * @filename GZIPcompress.java * @author Bruce Eckel * * ToDo Uses GZIP compression to compress a file * whose name is passed on the command line * * 【注】本例混用了 char-oriented streams 和 * byte-oriented streams:in 用的是 Reader, * 而 GZIPOutputStream 的 constructor 只接受 * OutputStream 而非 Writer。文件開啟後, * GZIPInputStream 會被轉換為 Reader。 */ import java.io.IOException; import java.io.BufferedReader; import java.io.BufferedOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.InputStreamReader; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; public class GZIPcompress { public static void main(String[] args) throws IOException { // 1)壓縮文件 BufferedReader in = new BufferedReader( new FileReader(args[0])); BufferedOutputStream out = new BufferedOutputStream( new GZIPOutputStream(new FileOutputStream("test.gz"))); System.out.println("Writing file"); int c; while((c = in.read()) != -1) out.write(c); in.close(); out.close(); // 2)顯示壓縮文件內容 System.out.println("Reading file"); BufferedReader in2 = new BufferedReader( new InputStreamReader(new GZIPInputStream( new FileInputStream("test.gz")))); String s; while((s = in2.readLine()) != null) System.out.println(s); } }
(2)Multifile storage with Zip
- java.util.zip 提供了一系列針對 Zip 格式的 classes;其中 CheckSum(interface)有兩類:Adler32(較快) 和 CRC32(較慢但更為精確)。
(3)Java ARchives (JARs)
- JAR 是由多個經過 Zip 壓縮的文件所組成的單一文件,它同時附有一份內部文件的清單(manifest)。其中 mainfest 可以自己建立,否則 JAR 程序會自動產生。
- Sun JDK 內附了一個 JAR 工具,其壓縮格式如下:
jar [options] destination [mainfest] inputfile(s)
option | function |
c | 創建新的文件 |
t | 列出文件的內容 |
x | 解壓所有文件 |
u | 更新既有的文件 |
v | 產生 jar 執行過程的完整信息 |
f | 指定歸檔的文件名。如果不進行指定,JAR 就會假設其輸入來自 standard input,並在建立文件時假定其輸出對象為 standard output |
m | 指定 manifest 文件名 |
O | 只存儲文件,並不進行 Zip 壓縮 |
M | 通知 JAR 不要自動產生 manifest 文件 |
i | 為指定的 JAR 文件生成索引信息 |
C | 指定歸檔的目錄(包含其子目錄) |
九、Object serialization
- Java 的 object serialization 機制可以將任何實現了 Serializable interface 的對象轉換為 bytes 序列,該序列可被還原為原來的對象,還可以通過網絡進行傳輸(object serialization 機制會自動處理 bytes 序列在不同 OS 上的差異)。
- Object serialization 可將 serializable 對象寫入磁盤,並於程序重新啟動時恢復其狀態;從而實現了 lightweight persistence(之所以稱其為 lightweight,是因為 serialize 和 deserialize 的動作需要手動完成)。
- Object serialization 使 Java 得以支持 RMI(Remote Method Invocation)和 JavaBeans。
- 對一個對象進行 serialization,只需它實現了 Serializable interface 即可;而 Serializable interface 本身並不具備任何的函數(這樣的 interface 也稱為 marker interface)。
- Object Serialization:
- serialize:產生某種 OutputStream 對象並以 ObjectOutputStream 對象加以包裝,然後調用 writeObject() 即可,輸出的數據將被送回該 OutputStream 中;
- deserialize:產生某種 InputStream 對象並以 ObjectInputStream 對象加以包裝,然後調用 readObject() 即可(readObject() 返回的是經過 upcasting 的對象,所以使用該對象前必須進行 downcasting)。
- Object serialization 不僅存儲對象在內存中的原始數據,還會追蹤該對象內含的 reference 所指的對象並將它們存儲,以此類推,整個對象網絡都會被存儲下來。
- Deserializing 並不會調用任何的 constructor(包括 defalut constructor ),整個對象的狀態都是通過讀取 InputStream 恢復的。
(1)Finding the class
- Deserializing 需要通過對象原始的 .class 文件,所以進行 deserializing 時必須首先確保 JVM 可以找到( local 的 classpath 或是 Internet )相應的 .class 文件。
(2)Controlling serialization
- 與 Serializable interface 相比,Externalizable interface(繼承自 Serializable interface )增加了 writeExternal() 和 readExternal() 兩個函數,用以控制 serialization 的過程。
- 與 Serializable interface 不同,使用 Externalizable interface 進行 deserializing 時,所有的 default constructor 都會被調用( Serializable interface 僅讀取 InputStream,並不會有任何的調用動作),然後才是 readExternal()。由於 Externalizable interface 的這個特點,因此在使用它的時候必須在 writeExternal() 和 readExternal() 中根據實際調用正確的 constructor。
- The transient keyword
- Serializtion 會略過由關鍵字 transient 聲明的數據(不進行存儲)。
- 因為缺省情況下 Externalizable 對象的值並不會被存儲,所以 transient 只用於 Serializable。
/* * @filename Logon.java * @author Bruce Eckel * * ToDo Demonstrates the "transient" keyword. */ import java.io.IOException; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.Date; class Logon implements Serializable { private Date date = new Date(); private String username; // transient 聲明的 password (值)在 serializing 時將不會被存儲 private transient String password; Logon(String name, String pwd) { username = name; password = pwd; } public String toString() { String pwd = (password == null) ? "(n/a)" : password; return "logon info: \n " + "username: " + username + "\n date: " + date + "\n password: " + pwd; } public static void main(String[] args) throws IOException, ClassNotFoundException { Logon a = new Logon("Hulk", "myLittlePony"); System.out.println("logon a = " + a); ObjectOutputStream o = new ObjectOutputStream( new FileOutputStream("Logon.out")); o.writeObject(a); o.close(); // Delay: int seconds = 5; long t = System.currentTimeMillis() + seconds * 1000; while(System.currentTimeMillis() < t) ; // Now get them back: ObjectInputStream in = new ObjectInputStream( new FileInputStream("Logon.out")); System.out.println("Recovering object at " + new Date()); a = (Logon) in.readObject(); System.out.println("logon a = " + a); } }
- Externalizable 的替代方案
- 實現 Serializable interface 並加入 private 的 writeObject() 和 readObject(),它們會取代 serialization 缺省的行為。
// 必須遵循以下的 signatures: private void writeObject(ObjectOutputStream stream) throws IOException; private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException;
- 調用 ObjectOutputStream.writeObject() 時,傳入的 Serializable 對象會被檢查以確認有無自己的 writeObject(),有則執行;同理於 readObject()。
- ObjectOutputStream.defaultWriteObject() 和 ObjectInputStream.defaultReadObject() 用於處理 non-static 和 non-transient 成員。
- 實現 Serializable interface 並加入 private 的 writeObject() 和 readObject(),它們會取代 serialization 缺省的行為。
- Versioning:Java 的 versioning 機制過於簡單,並不適用於所用的場合(詳見 JDK 的 HTML 說明文檔)。
(3)Using persistence
- Serialization 機制可以完全恢復單一 stream 中的對象網絡,並且不會額外的複製任何對象,也就是說單一 stream 中如果有多個對象擁有指向同一對象的 reference,該 reference 指向的對象僅會保存一份。對於非單一的 stream,由於系統無法知道各個 stream 中的對象其實是相同的,所以系統會製造出完全不同的對象網絡。
- 由於 static 成員並不屬於對象,所以 serializing 並不存儲 static 成員,因此需要自行提供 static 函數對其進行處理。
十、Tokenizing input
- Tokenizing:将 character 序列劃分為 token 序列(由選定的分隔符劃分而成的文本片段)。
(1)StreamTokenizer
- java.io.StreamTokenizer 並非繼承自 InputStream / OutputStream,但因為它只能處理 InputStream(Deprecated)和 Reader 對象,所以被歸類於 I/O。
(2)StringTokenizer
- java.util.StringTokenizer 一般只用於處理簡單的應用( String ),可被視為簡化的 java.io.StreamTokenizer。
(3)Checking capitalization style
【Summary】
- Java I/O 體系主要可分為 bytes-oriented 和 chars-oriented,應根據實際進行選擇,一般情況下支持 Unicode 的 chars-oriented classes 應當被優先考慮。
- Java I/O library 中大量使用了 decorator 模式。