六、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

  1. 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

  2. Input from memory
    • 通過 String(已含內容)產生 StringReader。

    • java.io.Reader(abstract class)的 read() 函數返回的是 int,所以使用 read() 時必須根據實際進行轉型。

  3. Formatted memory input
    • 讀取格式化數據通常使用的是 DataInputStream(byte-oriented),因此也就只能使用 InputStream classes 而不能使用 Reader classes。

    • 任何一個 Byte 對 DataInputStream 的 readByte() 函數來說都是合法的結果,所以不能憑它的返回值來判斷輸入是否結束。

    • DataInputStream 的 available()(繼承自 java.io.FilterInputStream)函數返回可供讀取的字符數。但它的運作方式取決於所輸入的對象,所以應當結合實際使用。

  4. File output
    • 產生一個 FileWriter 對象連接至文件,將其 reference 傳給 BufferedWriter 的 constructor(緩衝可以大幅提高 I/O 效能)。如果需要格式化輸出,可以再將 BufferedWriter 的 object reference 傳給 PrintWriter 的 constructor。(這樣產生的文件是文本文件)

    • 使用 Buffered 類的 classes 緩衝輸出文件後,必須調用 close() 函數關閉文件,否則緩衝區的內容可能不會被清空從而得不到完整的結果。

(2)Output streams

  1. 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() 讀取的方法會更為簡單。

  2. 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 成員。

  • 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 模式。