Skip to content
This repository was archived by the owner on Aug 25, 2019. It is now read-only.

Latest commit

 

History

History
2580 lines (2021 loc) · 127 KB

File metadata and controls

2580 lines (2021 loc) · 127 KB

第10章 Java IO系統

“對語言設計人員來說,創建好的輸入/輸出系統是一項特別困難的任務。”

由於存在大量不同的設計模式,所以該任務的困難性是很容易證明的。其中最大的挑戰似乎是如何覆蓋所有可能的因素。不僅有三種不同的種類的IO需要考慮(文件、控制檯、網絡連接),而且需要通過大量不同的方式與它們通信(順序、隨機訪問、二進制、字符、按行、按字等等)。

Java庫的設計者通過創建大量類來攻克這個難題。事實上,Java的IO系統採用瞭如此多的類,以致剛開始會產生不知從何處入手的感覺(具有諷刺意味的是,Java的IO設計初衷實際要求避免過多的類)。從Java 1.0升級到Java 1.1後,IO庫的設計也發生了顯著的變化。此時並非簡單地用新庫替換舊庫,Sun的設計人員對原來的庫進行了大手筆的擴展,添加了大量新的內容。因此,我們有時不得不混合使用新庫與舊庫,產生令人無奈的複雜代碼。

本章將幫助大家理解標準Java庫內的各種IO類,並學習如何使用它們。本章的第一部分將介紹“舊”的Java 1.0 IO流庫,因為現在有大量代碼仍在使用那個庫。本章剩下的部分將為大家引入Java 1.1 IO庫的一些新特性。注意若用Java 1.1編譯器來編譯本章第一部分介紹的部分代碼,可能會得到一條“不建議使用該特性”(Deprecated feature)警告消息。代碼仍然能夠使用;編譯器只是建議我們換用本章後面要講述的一些新特性。但我們這樣做是有價值的,因為可以更清楚地認識老方法與新方法之間的一些差異,從而加深我們的理解(並可順利閱讀為Java 1.0寫的代碼)。

10.1 輸入和輸出

可將Java庫的IO類分割為輸入與輸出兩個部分,這一點在用Web瀏覽器閱讀聯機Java類文檔時便可知道。通過繼承,從InputStream(輸入流)派生的所有類都擁有名為read()的基本方法,用於讀取單個字節或者字節數組。類似地,從OutputStream派生的所有類都擁有基本方法write(),用於寫入單個字節或者字節數組。然而,我們通常不會用到這些方法;它們之所以存在,是因為更復雜的類可以利用它們,以便提供一個更有用的接口。因此,我們很少用單個類創建自己的系統對象。一般情況下,我們都是將多個對象重疊在一起,提供自己期望的功能。我們之所以感到Java的流庫(Stream Library)異常複雜,正是由於為了創建單獨一個結果流,卻需要創建多個對象的緣故。

很有必要按照功能對類進行分類。庫的設計者首先決定與輸入有關的所有類都從InputStream繼承,而與輸出有關的所有類都從OutputStream繼承。

10.1.1 InputStream的類型

InputStream的作用是標誌那些從不同起源地產生輸入的類。這些起源地包括(每個都有一個相關的InputStream子類):

(1) 字節數組

(2) String對象

(3) 文件

(4) “管道”,它的工作原理與現實生活中的管道類似:將一些東西置入一端,它們在另一端出來。 (5) 一系列其他流,以便我們將其統一收集到單獨一個流內。

(6) 其他起源地,如Internet連接等(將在本書後面的部分講述)。

除此以外,FilterInputStream也屬於InputStream的一種類型,用它可為“析構器”類提供一個基類,以便將屬性或者有用的接口同輸入流連接到一起。這將在以後討論。

Class

Function

Constructor Arguments

How to use it

ByteArray-InputStream

Allows a buffer in memory to be used as an InputStream.

The buffer from which to extract the bytes.

As a source of data. Connect it to a FilterInputStream object to provide a useful interface.

StringBuffer-InputStream

Converts a String into an InputStream.

A String. The underlying implementation actually uses a StringBuffer.

As a source of data. Connect it to a FilterInputStream object to provide a useful interface.

File-InputStream

For reading information from a file.

A String representing the file name, or a File or FileDescriptor object.

As a source of data. Connect it to a FilterInputStream object to provide a useful interface.
功能 構造器參數/如何使用
`ByteArrayInputStream 允許內存中的一個緩衝區作為InputStream`使用 從中提取字節的緩衝區/作為一個數據源使用。通過將其同一個FilterInputStream對象連接,可提供一個有用的接口
StringBufferInputStream 將一個String轉換成InputStream 一個String(字符串)。基礎的實現方案實際採用一個
StringBuffer(字符串緩衝)/作為一個數據源使用。 通過將其同一個FilterInputStream對象連接,可提供一個有用的接口
FileInputStream 用於從文件讀取信息 代表文件名的一個String,或者一個FileFileDescriptor對象/作為一個數據源使用。通過將其同一個FilterInputStream對象連接,可提供一個有用的接口
Piped-InputStream

Produces the data that’s being written to the associated PipedOutput-Stream. Implements the “piping” concept.

PipedOutputStream

As a source of data in multithreading. Connect it to a FilterInputStream object to provide a useful interface.

Sequence-InputStream

Coverts two or more InputStream objects into a single InputStream.

Two InputStream objects or an Enumeration for a container of InputStream objects.

As a source of data. Connect it to a FilterInputStream object to provide a useful interface.

Filter-InputStream

Abstract class which is an interface for decorators that provide useful functionality to the other InputStream classes. See Table 10-3.

See Table 10-3.

See Table 10-3.
功能 構造器參數/如何使用
PipedInputString 產生為相關的PipedOutputStream寫的數據。實現了“管道化”的概念 PipedOutputStream/作為一個數據源使用。通過將其同一個FilterInputStream對象連接,可提供一個有用的接口
SequenceInputStream 將兩個或更多的InputStream對象轉換成單個InputStream使用 兩個InputStream對象或者一個Enumeration,用於InputStream對象的一個容器/作為一個數據源使用。通過將其同一個FilterInputStream對象連接,可提供一個有用的接口
FilterInputStream 對作為析構器接口使用的類進行抽象;那個析構器為其他InputStream類提供了有用的功能。參見表10.3 參見表10.3/參見表10.3

10.1.2 OutputStream的類型

這一類別包括的類決定了我們的輸入往何處去:一個字節數組(但沒有String;假定我們可用字節數組創建一個);一個文件;或者一個“管道”。

除此以外,FilterOutputStream為“析構器”類提供了一個基類,它將屬性或者有用的接口同輸出流連接起來。這將在以後討論。

表10.2 OutputStream的類型

Class

Function

Constructor Arguments

How to use it

ByteArray-OutputStream

Creates a buffer in memory. All the data that you send to the stream is placed in this buffer.

Optional initial size of the buffer.

To designate the destination of your data. Connect it to a FilterOutputStream object to provide a useful interface.

File-OutputStream

For sending information to a file.

A String representing the file name, or a File or FileDescriptor object.

To designate the destination of your data. Connect it to a FilterOutputStream object to provide a useful interface.

Piped-OutputStream

Any information you write to this automatically ends up as input for the associated PipedInput-Stream. Implements the “piping” concept.

PipedInputStream

To designate the destination of your data for multithreading. Connect it to a FilterOutputStream object to provide a useful interface.

Filter-OutputStream

Abstract class which is an interface for decorators that provide useful functionality to the other OutputStream classes. See Table
10-4.

See Table 10-4.

See Table 10-4.

| 類 | 功能 | 構造器參數 / 如何使用 | | --- | --- | --- | --- | | ByteArrayOutputStream | 在內存中創建一個緩衝區。我們發送給流的所有數據都會置入這個緩衝區。| 可選緩衝區的初始大小 / 用於指出數據的目的地。若將其同FilterOutputStream對象連接到一起,可提供一個有用的接口 | | FileOutputStream | 將信息發給一個文件 | 用一個String代表文件名,或選用一個FileFileDescriptor對象 / 用於指出數據的目的地。若將其同FilterOutputStream對象連接到一起,可提供一個有用的接口 | | PipedOutputStream | 我們寫給它的任何信息都會自動成為相關的PipedInputStream的輸出。實現了“管道化”的概念 | PipedInputStream/為多線程處理指出自己數據的目的地 / 將其同FilterOutputStream對象連接到一起,便可提供一個有用的接口 | FilterOutputStream | 對作為析構器接口使用的類進行抽象處理;那個析構器為其他OutputStream類提供了有用的功能。參見表10.4 | 參見表10.4 |

10.2 增添屬性和有用的接口

利用層次化對象動態和透明地添加單個對象的能力的做法叫作“裝飾器”(Decorator)方案——“方案”屬於本書第16章的主題(註釋①)。裝飾器方案規定封裝於初始化對象中的所有對象都擁有相同的接口,以便利用裝飾器的“透明”性質——我們將相同的消息發給一個對象,無論它是否已被“裝飾”。這正是在Java IO庫裡存在“過濾器”(Filter)類的原因:抽象的“過濾器”類是所有裝飾器的基類(裝飾器必須擁有與它裝飾的那個對象相同的接口,但裝飾器亦可對接口作出擴展,這種情況見諸於幾個特殊的“過濾器”類中)。

子類處理要求大量子類對每種可能的組合提供支持時,便經常會用到裝飾器——由於組合形式太多,造成子類處理變得不切實際。Java IO庫要求許多不同的特性組合方案,這正是裝飾器方案顯得特別有用的原因。但是,裝飾器方案也有自己的一個缺點。在我們寫一個程序的時候,裝飾器為我們提供了大得多的靈活性(因為可以方便地混合與匹配屬性),但它們也使自己的代碼變得更加複雜。原因在於Java IO庫操作不便,我們必須創建許多類——“核心”IO類型加上所有裝飾器——才能得到自己希望的單個IO對象。

FilterInputStreamFilterOutputStream(這兩個名字不十分直觀)提供了相應的裝飾器接口,用於控制一個特定的輸入流(InputStream)或者輸出流(OutputStream)。它們分別是從InputStreamOutputStream派生出來的。此外,它們都屬於抽象類,在理論上為我們與一個流的不同通信手段都提供了一個通用的接口。事實上,FilterInputStreamFilterOutputStream只是簡單地模仿了自己的基類,它們是一個裝飾器的基本要求。

10.2.1 通過FilterInputStreamInputStream裡讀入數據

FilterInputStream類要完成兩件全然不同的事情。其中,DataInputStream允許我們讀取不同的基本類型數據以及String對象(所有方法都以read開頭,比如readByte()readFloat()等等)。伴隨對應的DataOutputStream,我們可通過數據“流”將基本類型的數據從一個地方搬到另一個地方。這些“地方”是由表10.1總結的那些類決定的。若讀取塊內的數據,並自己進行解析,就不需要用到DataInputStream。但在其他許多情況下,我們一般都想用它對自己讀入的數據進行自動格式化。

剩下的類用於修改InputStream的內部行為方式:是否進行緩衝,是否跟蹤自己讀入的數據行,以及是否能夠推回一個字符等等。後兩種類看起來特別象提供對構建一個編譯器的支持(換言之,添加它們為了支持Java編譯器的構建),所以在常規編程中一般都用不著它們。

也許幾乎每次都要緩衝自己的輸入,無論連接的是哪個IO設備。所以IO庫最明智的做法就是將未緩衝輸入作為一種特殊情況處理,同時將緩衝輸入接納為標準做法。

表10.3 FilterInputStream的類型

Class

Function

Constructor Arguments

How to use it

Data-InputStream

Used in concert with DataOutputStream, so you can read primitives (int, char, long, etc.) from a stream in a portable fashion.

InputStream

Contains a full interface to allow you to read primitive types.


Buffered-InputStream

Use this to prevent a physical read every time you want more data. You’re saying “Use a buffer.”

InputStream, with optional buffer size.

This doesn’t provide an interface per se, just a requirement that a buffer be used. Attach an interface object.

LineNumber-InputStream

Keeps track of line numbers in the input stream; you can call getLineNumber( ) and setLineNumber(int).

InputStream

This just adds line numbering, so you’ll probably attach an interface object.

Pushback-InputStream

Has a one byte push-back buffer so that you can push back the last character read.

InputStream

Generally used in the scanner for a compiler and probably included because the Java compiler needed it. You probably won’t use this.
功能 構造器參數/如何使用
DataInputStream DataOutputStream聯合使用,使自己能以機動方式讀取一個流中的基本數據類型(intcharlong等等) InputStream/包含了一個完整的接口,以便讀取基本數據類型
BufferedInputStream 避免每次想要更多數據時都進行物理性的讀取,告訴它“請先在緩衝區裡找” InputStream,沒有可選的緩衝區大小/本身並不能提供一個接口,只是發出使用緩衝區的要求。要求同一個接口對象連接到一起
LineNumberInputStream 跟蹤輸入流中的行號;可調用getLineNumber()以及setLineNumber(int) 只是添加對數據行編號的能力,所以可能需要同一個真正的接口對象連接
PushbackInputStream 有一個字節的後推緩衝區,以便後推讀入的上一個字符 InputStream/通常由編譯器在掃描器中使用,因為Java編譯器需要它。一般不在自己的代碼中使用

10.2.2 通過FilterOutputStream向OutputStream裡寫入數據

DataInputStream對應的是DataOutputStream,後者對各個基本數據類型以及String對象進行格式化,並將其置入一個數據“流”中,以便任何機器上的DataInputStream都能正常地讀取它們。所有方法都以wirte開頭,例如writeByte()writeFloat()等等。

若想進行一些真正的格式化輸出,比如輸出到控制檯,請使用PrintStream。利用它可以打印出所有基本數據類型以及String對象,並可採用一種易於查看的格式。這與DataOutputStream正好相反,後者的目標是將那些數據置入一個數據流中,以便DataInputStream能夠方便地重新構造它們。System.out靜態對象是一個PrintStream

PrintStream內兩個重要的方法是print()println()。它們已進行了覆蓋處理,可打印出所有數據類型。print()println()之間的差異是後者在操作完畢後會自動添加一個新行。

BufferedOutputStream屬於一種“修改器”,用於指示數據流使用緩衝技術,使自己不必每次都向流內物理性地寫入數據。通常都應將它應用於文件處理和控制器IO。

表10.4 FilterOutputStream的類型

Class

Function

Constructor Arguments

How to use it

Data-OutputStream

Used in concert with DataInputStream so you can write primitives (int, char, long, etc.) to a stream in a portable fashion.

OutputStream

Contains full interface to allow you to write primitive types.

PrintStream

For producing formatted output. While DataOutputStream handles the storage of data, PrintStream handles display.

OutputStream, with optional boolean indicating that the buffer is flushed with every newline.

Should be the “final” wrapping for your OutputStream object. You’ll probably use this a lot.

Buffered-OutputStream

Use this to prevent a physical write every time you send a piece of data. You’re saying “Use a buffer.” You can call flush( ) to flush the buffer.

OutputStream, with optional buffer size.

This doesn’t provide an interface per se, just a requirement that a buffer is used. Attach an interface object.
功能 構造器參數/如何使用
DataOutputStream DataInputStream配合使用,以便採用方便的形式將基本數據類型(intcharlong等)寫入一個數據流 OutputStream/包含了完整接口,以便我們寫入基本數據類型
PrintStream 用於產生格式化輸出。 DataOutputStream控制的是數據的“存儲”,而PrintStream控制的是“顯示”
OutputStream 可選一個布爾參數,指示緩衝區是否與每個新行一同刷新/對於自己的OutputStream對象,應該用final將其封閉在內。可能經常都要用到它
BufferedOutputStream 用它避免每次發出數據的時候都要進行物理性的寫入,要求它“請先在緩衝區裡找”。可調用flush(),對緩衝區進行刷新 OutputStream,可選緩衝區大小/本身並不能提供一個接口,只是發出使用緩衝區的要求。需要同一個接口對象連接到一起

10.3 本身的缺陷:RandomAccessFile

RandomAccessFile用於包含了已知長度記錄的文件,以便我們能用seek()從一條記錄移至另一條;然後讀取或修改那些記錄。各記錄的長度並不一定相同;只要知道它們有多大以及置於文件何處即可。

首先,我們有點難以相信RandomAccessFile不屬於InputStream或者OutputStream分層結構的一部分。除了恰巧實現了DataInput以及DataOutput(這兩者亦由DataInputStreamDataOutputStream實現)接口之外,它們與那些分層結構並無什麼關係。它甚至沒有用到現有InputStreamOutputStream類的功能——採用的是一個完全不相干的類。該類屬於全新的設計,含有自己的全部(大多數為固有)方法。之所以要這樣做,是因為RandomAccessFile擁有與其他IO類型完全不同的行為,因為我們可在一個文件裡向前或向後移動。不管在哪種情況下,它都是獨立運作的,作為Object的一個“直接繼承人”使用。

從根本上說,RandomAccessFile類似DataInputStreamDataOutputStream的聯合使用。其中,getFilePointer()用於瞭解當前在文件的什麼地方,seek()用於移至文件內的一個新地點,而length()用於判斷文件的最大長度。此外,構造器要求使用另一個參數(與C的fopen()完全一樣),指出自己只是隨機讀("r"),還是讀寫兼施("rw")。這裡沒有提供對“只寫文件”的支持。也就是說,假如是從DataInputStream繼承的,那麼RandomAccessFile也有可能能很好地工作。

還有更難對付的。很容易想象我們有時要在其他類型的數據流中搜索,比如一個ByteArrayInputStream,但搜索方法只有RandomAccessFile才會提供。而後者只能針對文件才能操作,不能針對數據流操作。此時,BufferedInputStream確實允許我們標記一個位置(使用mark(),它的值容納於單個內部變量中),並用reset()重設那個位置。但這些做法都存在限制,並不是特別有用。

10.4 File類

File類有一個欺騙性的名字——通常會認為它對付的是一個文件,但實情並非如此。它既代表一個特定文件的名字,也代表目錄內一系列文件的名字。若代表一個文件集,便可用list()方法查詢這個集,返回的是一個字符串數組。之所以要返回一個數組,而非某個靈活的集合類,是因為元素的數量是固定的。而且若想得到一個不同的目錄列表,只需創建一個不同的File對象即可。事實上,FilePath(文件路徑)似乎是一個更好的名字。本節將向大家完整地例示如何使用這個類,其中包括相關的FilenameFilter(文件名過濾器)接口。

10.4.1 目錄列表器

現在假設我們想觀看一個目錄列表。可用兩種方式列出File對象。若在不含參數的情況下調用list(),會獲得File對象包含的一個完整列表。然而,若想對這個列表進行某些限制,就需要使用一個“目錄過濾器”,該類的作用是指出應如何選擇File對象來完成顯示。

下面是用於這個例子的代碼(或在執行該程序時遇到困難,請參考第3章3.1.2小節“賦值”):

//: DirList.java
// Displays directory listing
package c10;
import java.io.*;

public class DirList {
  public static void main(String[] args) {
    try {
      File path = new File(".");
      String[] list;
      if(args.length == 0)
        list = path.list();
      else
        list = path.list(new DirFilter(args[0]));
      for(int i = 0; i < list.length; i++)
        System.out.println(list[i]);
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
}

class DirFilter implements FilenameFilter {
  String afn;
  DirFilter(String afn) { this.afn = afn; }
  public boolean accept(File dir, String name) {
    // Strip path information:
    String f = new File(name).getName();
    return f.indexOf(afn) != -1;
  }
} ///:~

DirFilter類“實現”了interface FilenameFilter(關於接口的問題,已在第7章進行了詳述)。下面讓我們看看FilenameFilter接口有多麼簡單:

public interface FilenameFilter {
boolean accept(文件目錄, 字符串名);
}

它指出這種類型的所有對象都提供了一個名為accept()的方法。之所以要創建這樣的一個類,背後的全部原因就是把accept()方法提供給list()方法,使list()能夠“回調”accept(),從而判斷應將哪些文件名包括到列表中。因此,通常將這種技術稱為“回調”,有時也稱為“算子”(也就是說,DirFilter是一個算子,因為它唯一的作用就是容納一個方法)。由於list()採用一個FilenameFilter對象作為自己的參數使用,所以我們能傳遞實現了FilenameFilter的任何類的一個對象,用它決定(甚至在運行期)list()方法的行為方式。回調的目的是在代碼的行為上提供更大的靈活性。

通過DirFilter,我們看出儘管一個“接口”只包含了一系列方法,但並不侷限於只能寫那些方法(但是,至少必須提供一個接口內所有方法的定義。在這種情況下,DirFilter構造器也會創建)。

accept()方法必須接納一個File對象,用它指示用於尋找一個特定文件的目錄;並接納一個String,其中包含了要尋找之文件的名字。可決定使用或忽略這兩個參數之一,但有時至少要使用文件名。記住list()方法準備為目錄對象中的每個文件名調用

accept(),核實哪個應包含在內——具體由accept()返回的“布爾”結果決定。 為確定我們操作的只是文件名,其中沒有包含路徑信息,必須採用String對象,並在它的外部創建一個File對象。然後調用

getName(),它的作用是去除所有路徑信息(採用與平臺無關的方式)。隨後,accept()String類的indexOf()方法檢查文件名內部是否存在搜索字符串"afn"。若在字符串內找到afn,那麼返回值就是afn的起點索引;但假如沒有找到,返回值就是-1。注意這只是一個簡單的字符串搜索例子,未使用常見的表達式“通配符”方案,比如"fo?.b?r*";這種方案更難實現。

list()方法返回的是一個數組。可查詢這個數組的長度,然後在其中遍歷,選定數組元素。與C和C++的類似行為相比,這種於方法內外方便遊歷數組的行為無疑是一個顯著的進步。

(1) 匿名內部類

下例用一個匿名內部類(已在第7章講述)來重寫顯得非常理想。首先創建了一個filter()方法,它返回指向FilenameFilter的一個引用:

//: DirList2.java
// Uses Java 1.1 anonymous inner classes
import java.io.*;

public class DirList2 {
  public static FilenameFilter
  filter(final String afn) {
    // Creation of anonymous inner class:
    return new FilenameFilter() {
      String fn = afn;
      public boolean accept(File dir, String n) {
        // Strip path information:
        String f = new File(n).getName();
        return f.indexOf(fn) != -1;
      }
    }; // End of anonymous inner class
  }
  public static void main(String[] args) {
    try {
      File path = new File(".");
      String[] list;
      if(args.length == 0)
        list = path.list();
      else
        list = path.list(filter(args[0]));
      for(int i = 0; i < list.length; i++)
        System.out.println(list[i]);
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

注意filter()的參數必須是final。這一點是匿名內部類要求的,使其能使用來自本身作用域以外的一個對象。

之所以認為這樣做更好,是由於FilenameFilter類現在同DirList2緊密地結合在一起。然而,我們可採取進一步的操作,將匿名內部類定義成list()的一個參數,使其顯得更加精簡。如下所示:

//: DirList3.java
// Building the anonymous inner class "in-place"
import java.io.*;

public class DirList3 {
  public static void main(final String[] args) {
    try {
      File path = new File(".");
      String[] list;
      if(args.length == 0)
        list = path.list();
      else
        list = path.list(
          new FilenameFilter() {
            public boolean
            accept(File dir, String n) {
              String f = new File(n).getName();
              return f.indexOf(args[0]) != -1;
            }
          });
      for(int i = 0; i < list.length; i++)
        System.out.println(list[i]);
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

main()現在的參數是final,因為匿名內部類直接使用args[0]

這展示瞭如何利用匿名內部類快速創建精簡的類,以便解決一些複雜的問題。由於Java中的所有東西都與類有關,所以它無疑是一種相當有用的編碼技術。它的一個好處是將特定的問題隔離在一個地方統一解決。但在另一方面,這樣生成的代碼不是十分容易閱讀,所以使用時必須慎重。

(2) 順序目錄列表

經常都需要文件名以排好序的方式提供。由於Java 1.0和Java 1.1都沒有提供對排序的支持(從Java 1.2開始提供),所以必須用第8章創建的SortVector將這一能力直接加入自己的程序。就象下面這樣:

//: SortedDirList.java
// Displays sorted directory listing
import java.io.*;
import c08.*;

public class SortedDirList {
  private File path;
  private String[] list;
  public SortedDirList(final String afn) {
    path = new File(".");
    if(afn == null)
      list = path.list();
    else
      list = path.list(
          new FilenameFilter() {
            public boolean
            accept(File dir, String n) {
              String f = new File(n).getName();
              return f.indexOf(afn) != -1;
            }
          });
    sort();
  }
  void print() {
    for(int i = 0; i < list.length; i++)
      System.out.println(list[i]);
  }
  private void sort() {
    StrSortVector sv = new StrSortVector();
    for(int i = 0; i < list.length; i++)
      sv.addElement(list[i]);
    // The first time an element is pulled from
    // the StrSortVector the list is sorted:
    for(int i = 0; i < list.length; i++)
      list[i] = sv.elementAt(i);
  }
  // Test it:
  public static void main(String[] args) {
    SortedDirList sd;
    if(args.length == 0)
      sd = new SortedDirList(null);
    else
      sd = new SortedDirList(args[0]);
    sd.print();
  }
} ///:~

這裡進行了另外少許改進。不再是將path(路徑)和list(列表)創建為main()的本地變量,它們變成了類的成員,使它們的值能在對象“生存”期間方便地訪問。事實上,main()現在只是對類進行測試的一種方式。大家可以看到,一旦列表創建完畢,類的構造器就會自動開始對列表進行排序。

這種排序不要求區分大小寫,所以最終不會得到一組全部單詞都以大寫字母開頭的列表,跟著是全部以小寫字母開頭的列表。然而,我們注意到在以相同字母開頭的一組文件名中,大寫字母是排在前面的——這對標準的排序來說仍是一種不合格的行為。Java 1.2已成功解決了這個問題。

10.4.2 檢查與創建目錄

File類並不僅僅是對現有目錄路徑、文件或者文件組的一個表示。亦可用一個File對象新建一個目錄,甚至創建一個完整的目錄路徑——假如它尚不存在的話。亦可用它瞭解文件的屬性(長度、上一次修改日期、讀/寫屬性等),檢查一個File對象到底代表一個文件還是一個目錄,以及刪除一個文件等等。下列程序完整展示瞭如何運用File類剩下的這些方法:

//: MakeDirectories.java
// Demonstrates the use of the File class to
// create directories and manipulate files.
import java.io.*;

public class MakeDirectories {
  private final static String usage =
    "Usage:MakeDirectories path1 ...\n" +
    "Creates each path\n" +
    "Usage:MakeDirectories -d path1 ...\n" +
    "Deletes each path\n" +
    "Usage:MakeDirectories -r path1 path2\n" +
    "Renames from path1 to path2\n";
  private static void usage() {
    System.err.println(usage);
    System.exit(1);
  }
  private static void fileData(File f) {
    System.out.println(
      "Absolute path: " + f.getAbsolutePath() +
      "\n Can read: " + f.canRead() +
      "\n Can write: " + f.canWrite() +
      "\n getName: " + f.getName() +
      "\n getParent: " + f.getParent() +
      "\n getPath: " + f.getPath() +
      "\n length: " + f.length() +
      "\n lastModified: " + f.lastModified());
    if(f.isFile())
      System.out.println("it's a file");
    else if(f.isDirectory())
      System.out.println("it's a directory");
  }
  public static void main(String[] args) {
    if(args.length < 1) usage();
    if(args[0].equals("-r")) {
      if(args.length != 3) usage();
      File
        old = new File(args[1]),
        rname = new File(args[2]);
      old.renameTo(rname);
      fileData(old);
      fileData(rname);
      return; // Exit main
    }
    int count = 0;
    boolean del = false;
    if(args[0].equals("-d")) {
      count++;
      del = true;
    }
    for( ; count < args.length; count++) {
      File f = new File(args[count]);
      if(f.exists()) {
        System.out.println(f + " exists");
        if(del) {
          System.out.println("deleting..." + f);
          f.delete();
        }
      }
      else { // Doesn't exist
        if(!del) {
          f.mkdirs();
          System.out.println("created " + f);
        }
      }
      fileData(f);
    }  
  }
} ///:~

fileData()中,可看到應用了各種文件調查方法來顯示與文件或目錄路徑有關的信息。

main()應用的第一個方法是renameTo(),利用它可以重命名(或移動)一個文件至一個全新的路徑(該路徑由參數決定),它屬於另一個File對象。這也適用於任何長度的目錄。

若試驗上述程序,就可發現自己能製作任意複雜程度的一個目錄路徑,因為mkdirs()會幫我們完成所有工作。在Java 1.0中,-d標誌報告目錄雖然已被刪除,但它依然存在;但在Java 1.1中,目錄會被實際刪除。

10.5 IO流的典型應用

儘管庫內存在大量IO流類,可通過多種不同的方式組合到一起,但實際上只有幾種方式才會經常用到。然而,必須小心在意才能得到正確的組合。下面這個相當長的例子展示了典型IO配置的創建與使用,可在寫自己的代碼時將其作為一個參考使用。注意每個配置都以一個註釋形式的編號起頭,並提供了適當的解釋信息。

//: IOStreamDemo.java
// Typical IO Stream Configurations
import java.io.*;
import com.bruceeckel.tools.*;

public class IOStreamDemo {
  public static void main(String[] args) {
    try {
      // 1. Buffered input file
      DataInputStream in =
        new DataInputStream(
          new BufferedInputStream(
            new FileInputStream(args[0])));
      String s, s2 = new String();
      while((s = in.readLine())!= null)
        s2 += s + "\n";
      in.close();

      // 2. Input from memory
      StringBufferInputStream in2 =
          new StringBufferInputStream(s2);
      int c;
      while((c = in2.read()) != -1)
        System.out.print((char)c);

      // 3. Formatted memory input
      try {
        DataInputStream in3 =
          new DataInputStream(
            new StringBufferInputStream(s2));
        while(true)
          System.out.print((char)in3.readByte());
      } catch(EOFException e) {
        System.out.println(
          "End of stream encountered");
      }

      // 4. Line numbering & file output
      try {
        LineNumberInputStream li =
          new LineNumberInputStream(
            new StringBufferInputStream(s2));
        DataInputStream in4 =
          new DataInputStream(li);
        PrintStream out1 =
          new PrintStream(
            new BufferedOutputStream(
              new FileOutputStream(
                "IODemo.out")));
        while((s = in4.readLine()) != null )
          out1.println(
            "Line " + li.getLineNumber() + s);
        out1.close(); // finalize() not reliable!
      } catch(EOFException e) {
        System.out.println(
          "End of stream encountered");
      }

      // 5. Storing & recovering data
      try {
        DataOutputStream out2 =
          new DataOutputStream(
            new BufferedOutputStream(
              new FileOutputStream("Data.txt")));
        out2.writeBytes(
          "Here's the value of pi: \n");
        out2.writeDouble(3.14159);
        out2.close();
        DataInputStream in5 =
          new DataInputStream(
            new BufferedInputStream(
              new FileInputStream("Data.txt")));
        System.out.println(in5.readLine());
        System.out.println(in5.readDouble());
      } catch(EOFException e) {
        System.out.println(
          "End of stream encountered");
      }

      // 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();

      // 7. File input shorthand
      InFile in6 = new InFile(args[0]);
      String s3 = new String();
      System.out.println(
        "First line in file: " +
        in6.readLine());
        in6.close();

      // 8. Formatted file output shorthand
      PrintFile out3 = new PrintFile("Data2.txt");
      out3.print("Test of PrintFile");
      out3.close();

      // 9. Data file output shorthand
      OutFile out4 = new OutFile("Data3.txt");
      out4.writeBytes("Test of outDataFile\n\r");
      out4.writeChars("Test of outDataFile\n\r");
      out4.close();

    } catch(FileNotFoundException e) {
      System.out.println(
        "File Not Found:" + args[0]);
    } catch(IOException e) {
      System.out.println("IO Exception");
    }
  }
} ///:~

10.5.1 輸入流

當然,我們經常想做的一件事情是將格式化的輸出打印到控制檯,但那已在第5章創建的com.bruceeckel.tools中得到了簡化。

第1到第4部分演示了輸入流的創建與使用(儘管第4部分展示了將輸出流作為一個測試工具的簡單應用)。

(1) 緩衝的輸入文件

為打開一個文件以便輸入,需要使用一個FileInputStream,同時將一個StringFile對象作為文件名使用。為提高速度,最好先對文件進行緩衝處理,從而獲得用於一個BufferedInputStream的構造器的結果引用。為了以格式化的形式讀取輸入數據,我們將那個結果引用賦給用於一個DataInputStream的構造器。DataInputStream是我們的最終(final)對象,並是我們進行讀取操作的接口。

在這個例子中,只用到了readLine()方法,但理所當然任何DataInputStream方法都可以採用。一旦抵達文件末尾,readLine()就會返回一個null(空),以便中止並退出while循環。

String s2用於聚集完整的文件內容(包括必須添加的新行,因為readLine()去除了那些行)。隨後,在本程序的後面部分中使用s2。最後,我們調用close(),用它關閉文件。從技術上說,會在運行finalize()時調用close()。而且我們希望一旦程序退出,就發生這種情況(無論是否進行垃圾收集)。然而,Java 1.0有一個非常突出的錯誤(Bug),造成這種情況不會發生。在Java 1.1中,必須明確調用System.runFinalizersOnExit(true),用它保證會為系統中的每個對象調用finalize()。然而,最安全的方法還是為文件明確調用close()

(2) 從內存輸入

這一部分採用已經包含了完整文件內容的String s2,並用它創建一個StringBufferInputStream(字符串緩衝輸入流)——作為構造器的參數,要求使用一個String,而非一個StringBuffer)。隨後,我們用read()依次讀取每個字符,並將其發送至控制檯。注意read()將下一個字節返回為int,所以必須將其轉換為一個char,以便正確地打印。

(3) 格式化內存輸入

StringBufferInputStream的接口是有限的,所以通常需要將其封裝到一個DataInputStream內,從而增強它的能力。然而,若選擇用readByte()每次讀出一個字符,那麼所有值都是有效的,所以不可再用返回值來偵測何時結束輸入。相反,可用available()方法判斷有多少字符可用。下面這個例子展示瞭如何從文件中一次讀出一個字符:

//: TestEOF.java
// Testing for the end of file while reading
// a byte at a time.
import java.io.*;

public class TestEOF {
  public static void main(String[] args) {
    try {
      DataInputStream in =
        new DataInputStream(
         new BufferedInputStream(
          new FileInputStream("TestEof.java")));
      while(in.available() != 0)
        System.out.print((char)in.readByte());
    } catch (IOException e) {
      System.err.println("IOException");
    }
  }
} ///:~

注意取決於當前從什麼媒體讀入,avaiable()的工作方式也是有所區別的。它在字面上意味著“可以不受阻塞讀取的字節數量”。對一個文件來說,它意味著整個文件。但對一個不同種類的數據流來說,它卻可能有不同的含義。因此在使用時應考慮周全。

為了在這樣的情況下偵測輸入的結束,也可以通過捕獲一個異常來實現。然而,若真的用異常來控制數據流,卻顯得有些大材小用。

(4) 行的編號與文件輸出

這個例子展示瞭如何LineNumberInputStream來跟蹤輸入行的編號。在這裡,不可簡單地將所有構造器都組合起來,因為必須保持LineNumberInputStream的一個引用(注意這並非一種繼承環境,所以不能簡單地將in4轉換到一個LineNumberInputStream)。因此,li容納了指向LineNumberInputStream的引用,然後在它的基礎上創建一個DataInputStream,以便讀入數據。

這個例子也展示瞭如何將格式化數據寫入一個文件。首先創建了一個FileOutputStream,用它同一個文件連接。考慮到效率方面的原因,它生成了一個BufferedOutputStream。這幾乎肯定是我們一般的做法,但卻必須明確地這樣做。隨後為了進行格式化,它轉換成一個PrintStream。用這種方式創建的數據文件可作為一個原始的文本文件讀取。

標誌DataInputStream何時結束的一個方法是readLine()。一旦沒有更多的字符串可以讀取,它就會返回null。每個行都會伴隨自己的行號打印到文件裡。該行號可通過li查詢。

可看到用於out1的、一個明確指定的close()。若程序準備掉轉頭來,並再次讀取相同的文件,這種做法就顯得相當有用。然而,該程序直到結束也沒有檢查文件IODemo.txt。正如以前指出的那樣,如果不為自己的所有輸出文件調用close(),就可能發現緩衝區不會得到刷新,造成它們不完整。。

10.5.2 輸出流

兩類主要的輸出流是按它們寫入數據的方式劃分的:一種按人的習慣寫入,另一種為了以後由一個DataInputStream而寫入。RandomAccessFile是獨立的,儘管它的數據格式兼容於DataInputStreamDataOutputStream

(5) 保存與恢復數據

PrintStream能格式化數據,使其能按我們的習慣閱讀。但為了輸出數據,以便由另一個數據流恢復,則需用一個DataOutputStream寫入數據,並用一個DataInputStream恢復(獲取)數據。當然,這些數據流可以是任何東西,但這裡採用的是一個文件,並進行了緩衝處理,以加快讀寫速度。

注意字符串是用writeBytes()寫入的,而非writeChars()。若使用後者,寫入的就是16位Unicode字符。由於DataInputStream中沒有補充的readChars方法,所以不得不用readChar()每次取出一個字符。所以對ASCII來說,更方便的做法是將字符作為字節寫入,在後面跟隨一個新行;然後再用readLine()將字符當作普通的ASCII行讀回。

writeDouble()double數字保存到數據流中,並用補充的readDouble()恢復它。但為了保證任何讀方法能夠正常工作,必須知道數據項在流中的準確位置,因為既有可能將保存的double數據作為一個簡單的字節序列讀入,也有可能作為char或其他格式讀入。所以必須要麼為文件中的數據採用固定的格式,要麼將額外的信息保存到文件中,以便正確判斷數據的存放位置。

(6) 讀寫隨機訪問文件

正如早先指出的那樣,RandomAccessFile與IO層次結構的剩餘部分幾乎是完全隔離的,儘管它也實現了DataInput和DataOutput接口。所以不可將其與InputStreamOutputStream子類的任何部分關聯起來。儘管也許能將一個ByteArrayInputStream當作一個隨機訪問元素對待,但只能用RandomAccessFile打開一個文件。必須假定RandomAccessFile已得到了正確的緩衝,因為我們不能自行選擇。

可以自行選擇的是第二個構造器參數:可決定以“只讀”(r)方式或“讀寫”(rw)方式打開一個RandomAccessFile文件。

使用RandomAccessFile的時候,類似於組合使用DataInputStreamDataOutputStream(因為它實現了等同的接口)。除此以外,還可看到程序中使用了seek(),以便在文件中到處移動,對某個值作出修改。

10.5.3 快捷文件處理

由於以前採用的一些典型形式都涉及到文件處理,所以大家也許會懷疑為什麼要進行那麼多的代碼輸入——這正是裝飾器方案一個缺點。本部分將向大家展示如何創建和使用典型文件讀取和寫入配置的快捷版本。這些快捷版本均置入packagecom.bruceeckel.tools中(自第5章開始創建)。為了將每個類都添加到庫內,只需將其置入適當的目錄,並添加對應的package語句即可。

(7) 快速文件輸入

若想創建一個對象,用它從一個緩衝的DataInputStream中讀取一個文件,可將這個過程封裝到一個名為InFile的類內。如下所示:

//: InFile.java
// Shorthand class for opening an input file
package com.bruceeckel.tools;
import java.io.*;

public class InFile extends DataInputStream {
  public InFile(String filename)
    throws FileNotFoundException {
    super(
      new BufferedInputStream(
        new FileInputStream(filename)));
  }
  public InFile(File file)
    throws FileNotFoundException {
    this(file.getPath());
  }
} ///:~

無論構造器的String版本還是File版本都包括在內,用於共同創建一個FileInputStream

就象這個例子展示的那樣,現在可以有效減少創建文件時由於重複強調造成的問題。

(8) 快速輸出格式化文件

亦可用同類型的方法創建一個PrintStream,令其寫入一個緩衝文件。下面是對com.bruceeckel.tools的擴展:

//: PrintFile.java
// Shorthand class for opening an output file
// for human-readable output.
package com.bruceeckel.tools;
import java.io.*;

public class PrintFile extends PrintStream {
  public PrintFile(String filename)
    throws IOException {
    super(
      new BufferedOutputStream(
        new FileOutputStream(filename)));
  }
  public PrintFile(File file)
    throws IOException {
    this(file.getPath());
  }
} ///:~

注意構造器不可能捕獲一個由基類構造器“拋”出的異常。

(9) 快速輸出數據文件

最後,利用類似的快捷方式可創建一個緩衝輸出文件,用它保存數據(與由人觀看的數據格式相反):

//: OutFile.java
// Shorthand class for opening an output file
// for data storage.
package com.bruceeckel.tools;
import java.io.*;

public class OutFile extends DataOutputStream {
  public OutFile(String filename)
    throws IOException {
    super(
      new BufferedOutputStream(
        new FileOutputStream(filename)));
  }
  public OutFile(File file)
    throws IOException {
    this(file.getPath());
  }
} ///:~

非常奇怪的是(也非常不幸),Java庫的設計者居然沒想到將這些便利措施直接作為他們的一部分標準提供。

10.5.4 從標準輸入中讀取數據

以Unix首先倡導的“標準輸入”、“標準輸出”以及“標準錯誤輸出”概念為基礎,Java提供了相應的System.inSystem.out以及System.err。貫這一整本書,大家都會接觸到如何用System.out進行標準輸出,它已預封裝成一個PrintStream對象。

System.err同樣是一個PrintStream,但System.in是一個原始的InputStream,未進行任何封裝處理。這意味著儘管能直接使用System.outSystem.err,但必須事先封裝System.in,否則不能從中讀取數據。

典型情況下,我們希望用readLine()每次讀取一行輸入信息,所以需要將System.in封裝到一個DataInputStream中。這是Java 1.0進行行輸入時採取的“老”辦法。在本章稍後,大家還會看到Java 1.1的解決方案。下面是個簡單的例子,作用是迴應我們鍵入的每一行內容:

//: Echo.java
// How to read from standard input
import java.io.*;

public class Echo {
  public static void main(String[] args) {
    DataInputStream in =
      new DataInputStream(
        new BufferedInputStream(System.in));
    String s;
    try {
      while((s = in.readLine()).length() != 0)
        System.out.println(s);
      // An empty line terminates the program
    } catch(IOException e) {
      e.printStackTrace();
    }
  }
} ///:~

之所以要使用try塊,是由於readLine()可能“拋”出一個IOException。注意同其他大多數流一樣,也應對System.in進行緩衝。

由於在每個程序中都要將System.in封裝到一個DataInputStream內,所以顯得有點不方便。但採用這種設計模式,可以獲得最大的靈活性。

10.5.5 管道數據流

本章已簡要介紹了PipedInputStream(管道輸入流)和PipedOutputStream(管道輸出流)。儘管描述不十分詳細,但並不是說它們作用不大。然而,只有在掌握了多線程處理的概念後,才可真正體會它們的價值所在。原因很簡單,因為管道化的數據流就是用於線程之間的通信。這方面的問題將在第14章用一個示例說明。

10.6 StreamTokenizer

儘管StreamTokenizer並不是從InputStreamOutputStream派生的,但它只隨同InputStream工作,所以十分恰當地包括在庫的IO部分中。

StreamTokenizer類用於將任何InputStream分割為一系列“記號”(Token)。這些記號實際是一些斷續的文本塊,中間用我們選擇的任何東西分隔。例如,我們的記號可以是單詞,中間用空白(空格)以及標點符號分隔。 下面是一個簡單的程序,用於計算各個單詞在文本文件中重複出現的次數:

//: SortedWordCount.java
// Counts words in a file, outputs
// results in sorted form.
import java.io.*;
import java.util.*;
import c08.*; // Contains StrSortVector

class Counter {
  private int i = 1;
  int read() { return i; }
  void increment() { i++; }
}

public class SortedWordCount {
  private FileInputStream file;
  private StreamTokenizer st;
  private Hashtable counts = new Hashtable();
  SortedWordCount(String filename)
    throws FileNotFoundException {
    try {
      file = new FileInputStream(filename);
      st = new StreamTokenizer(file);
      st.ordinaryChar('.');
      st.ordinaryChar('-');
    } catch(FileNotFoundException e) {
      System.out.println(
        "Could not open " + filename);
      throw e;
    }
  }
  void cleanup() {
    try {
      file.close();
    } catch(IOException e) {
      System.out.println(
        "file.close() unsuccessful");
    }
  }
  void countWords() {
    try {
      while(st.nextToken() !=
        StreamTokenizer.TT_EOF) {
        String s;
        switch(st.ttype) {
          case StreamTokenizer.TT_EOL:
            s = new String("EOL");
            break;
          case StreamTokenizer.TT_NUMBER:
            s = Double.toString(st.nval);
            break;
          case StreamTokenizer.TT_WORD:
            s = st.sval; // Already a String
            break;
          default: // single character in ttype
            s = String.valueOf((char)st.ttype);
        }
        if(counts.containsKey(s))
          ((Counter)counts.get(s)).increment();
        else
          counts.put(s, new Counter());
      }
    } catch(IOException e) {
      System.out.println(
        "st.nextToken() unsuccessful");
    }
  }
  Enumeration values() {
    return counts.elements();
  }
  Enumeration keys() { return counts.keys(); }
  Counter getCounter(String s) {
    return (Counter)counts.get(s);
  }
  Enumeration sortedKeys() {
    Enumeration e = counts.keys();
    StrSortVector sv = new StrSortVector();
    while(e.hasMoreElements())
      sv.addElement((String)e.nextElement());
    // This call forces a sort:
    return sv.elements();
  }
  public static void main(String[] args) {
    try {
      SortedWordCount wc =
        new SortedWordCount(args[0]);
      wc.countWords();
      Enumeration keys = wc.sortedKeys();
      while(keys.hasMoreElements()) {
        String key = (String)keys.nextElement();
        System.out.println(key + ": "
                 + wc.getCounter(key).read());
      }
      wc.cleanup();
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

最好將結果按排序格式輸出,但由於Java 1.0和Java 1.1都沒有提供任何排序方法,所以必須由自己動手。這個目標可用一個StrSortVector方便地達成(創建於第8章,屬於那一章創建的軟件包的一部分。記住本書所有子目錄的起始目錄都必須位於類路徑中,否則程序將不能正確地編譯)。

為打開文件,使用了一個FileInputStream。而且為了將文件轉換成單詞,從FileInputStream中創建了一個StreamTokenizer。在StreamTokenizer中,存在一個默認的分隔符列表,我們可用一系列方法加入更多的分隔符。在這裡,我們用ordinaryChar()指出“該字符沒有特別重要的意義”,所以解析器不會把它當作自己創建的任何單詞的一部分。例如,st.ordinaryChar('.')表示小數點不會成為解析出來的單詞的一部分。在與Java配套提供的聯機文檔中,可以找到更多的相關信息。

countWords()中,每次從數據流中取出一個記號,而ttype信息的作用是判斷對每個記號採取什麼操作——因為記號可能代表一個行尾、一個數字、一個字符串或者一個字符。

找到一個記號後,會查詢Hashtable counts,核實其中是否已經以“鍵”(Key)的形式包含了一個記號。若答案是肯定的,對應的Counter(計數器)對象就會自增,指出已找到該單詞的另一個實例。若答案為否,則新建一個Counter——因為Counter構造器會將它的值初始化為1,正是我們計算單詞數量時的要求。

SortedWordCount並不屬於Hashtable(散列表)的一種類型,所以它不會繼承。它執行的一種特定類型的操作,所以儘管keys()values()方法都必須重新揭示出來,但仍不表示應使用那個繼承,因為大量Hashtable方法在這裡都是不適當的。除此以外,對於另一些方法來說(比如getCounter()——用於獲得一個特定字符串的計數器;又如sortedKeys()——用於產生一個枚舉),它們最終都改變了SortedWordCount接口的形式。

main()內,我們用SortedWordCount打開和計算文件中的單詞數量——總共只用了兩行代碼。隨後,我們為一個排好序的鍵(單詞)列表提取出一個枚舉。並用它獲得每個鍵以及相關的Count(計數)。注意必須調用cleanup(),否則文件不能正常關閉。 採用了StreamTokenizer的第二個例子將在第17章提供。

10.6.1 StringTokenizer

儘管並不必要IO庫的一部分,但StringTokenizer提供了與StreamTokenizer極相似的功能,所以在這裡一併講述。

StringTokenizer的作用是每次返回字符串內的一個記號。這些記號是一些由製表站、空格以及新行分隔的連續字符。因此,字符串"Where is my cat?"的記號分別是"Where""is""my""cat?"。與StreamTokenizer類似,我們可以指示StringTokenizer按照我們的願望分割輸入。但對於StringTokenizer,卻需要向構造器傳遞另一個參數,即我們想使用的分隔字符串。通常,如果想進行更復雜的操作,應使用StreamTokenizer

可用nextToken()StringTokenizer對象請求字符串內的下一個記號。該方法要麼返回一個記號,要麼返回一個空字符串(表示沒有記號剩下)。

作為一個例子,下述程序將執行一個有限的句法分析,查詢鍵短語序列,瞭解句子暗示的是快樂亦或悲傷的含義。

//: AnalyzeSentence.java
// Look for particular sequences
// within sentences.
import java.util.*;

public class AnalyzeSentence {
  public static void main(String[] args) {
    analyze("I am happy about this");
    analyze("I am not happy about this");
    analyze("I am not! I am happy");
    analyze("I am sad about this");
    analyze("I am not sad about this");
    analyze("I am not! I am sad");
    analyze("Are you happy about this?");
    analyze("Are you sad about this?");
    analyze("It's you! I am happy");
    analyze("It's you! I am sad");
  }
  static StringTokenizer st;
  static void analyze(String s) {
    prt("\nnew sentence >> " + s);
    boolean sad = false;
    st = new StringTokenizer(s);
    while (st.hasMoreTokens()) {
      String token = next();
      // Look until you find one of the
      // two starting tokens:
      if(!token.equals("I") &&
         !token.equals("Are"))
        continue; // Top of while loop
      if(token.equals("I")) {
        String tk2 = next();
        if(!tk2.equals("am")) // Must be after I
          break; // Out of while loop
        else {
          String tk3 = next();
          if(tk3.equals("sad")) {
            sad = true;
            break; // Out of while loop
          }
          if (tk3.equals("not")) {
            String tk4 = next();
            if(tk4.equals("sad"))
              break; // Leave sad false
            if(tk4.equals("happy")) {
              sad = true;
              break;
            }
          }
        }
      }
      if(token.equals("Are")) {
        String tk2 = next();
        if(!tk2.equals("you"))
          break; // Must be after Are
        String tk3 = next();
        if(tk3.equals("sad"))
          sad = true;
        break; // Out of while loop
      }
    }
    if(sad) prt("Sad detected");
  }
  static String next() {
    if(st.hasMoreTokens()) {
      String s = st.nextToken();
      prt(s);
      return s;
    }
    else
      return "";
  }
  static void prt(String s) {
    System.out.println(s);
  }
} ///:~

對於準備分析的每個字符串,我們進入一個while循環,並將記號從那個字符串中取出。請注意第一個if語句,假如記號既不是"I",也不是"Are",就會執行continue(返回循環起點,再一次開始)。這意味著除非發現一個"I"或者"Are",才會真正得到記號。大家可能想用==代替equals()方法,但那樣做會出現不正常的表現,因為==比較的是引用值,而equals()比較的是內容。

analyze()方法剩餘部分的邏輯是搜索"I am sad"(我很憂傷、"I am nothappy"(我不快樂)或者"Are you sad?"(你悲傷嗎?)這樣的句法格式。若沒有break語句,這方面的代碼甚至可能更加散亂。大家應注意對一個典型的解析器來說,通常都有這些記號的一個表格,並能在讀取新記號的時候用一小段代碼在表格內移動。

無論如何,只應將StringTokenizer看作StreamTokenizer一種簡單而且特殊的簡化形式。然而,如果有一個字符串需要進行記號處理,而且StringTokenizer的功能實在有限,那麼應該做的全部事情就是用StringBufferInputStream將其轉換到一個數據流裡,再用它創建一個功能更強大的StreamTokenizer

10.7 Java 1.1的IO流

到這個時候,大家或許會陷入一種困境之中,懷疑是否存在IO流的另一種設計模式,並可能要求更大的代碼量。還有人能提出一種更古怪的設計嗎?事實上,Java 1.1對IO流庫進行了一些重大的改進。看到ReaderWriter類時,大多數人的第一個印象(就象我一樣)就是它們用來替換原來的InputStreamOutputStream類。但實情並非如此。儘管不建議使用原始數據流庫的某些功能(如使用它們,會從編譯器收到一條警告消息),但原來的數據流依然得到了保留,以便維持向後兼容,而且:

(1) 在老式層次結構里加入了新類,所以Sun公司明顯不會放棄老式數據流。

(2) 在許多情況下,我們需要與新結構中的類聯合使用老結構中的類。為達到這個目的,需要使用一些“橋”類:

InputStreamReader將一個InputStream轉換成ReaderOutputStreamWriter將一個OutputStream轉換成Writer。 所以與原來的IO流庫相比,經常都要對新IO流進行層次更多的封裝。同樣地,這也屬於裝飾器方案的一個缺點——需要為額外的靈活性付出代價。

之所以在Java 1.1裡添加了ReaderWriter層次,最重要的原因便是國際化的需求。老式IO流層次結構只支持8位字節流,不能很好地控制16位Unicode字符。由於Unicode主要面向的是國際化支持(Java內含的char是16位的Unicode),所以添加了ReaderWriter層次,以提供對所有IO操作中的Unicode的支持。除此之外,新庫也對速度進行了優化,可比舊庫更快地運行。

與本書其他地方一樣,我會試著提供對類的一個概述,但假定你會利用聯機文檔搞定所有的細節,比如方法的詳盡列表等。

10.7.1 數據的發起與接收

Java 1.0的幾乎所有IO流類都有對應的Java 1.1類,用於提供內建的Unicode管理。似乎最容易的事情就是“全部使用新類,再也不要用舊的”,但實際情況並沒有這麼簡單。有些時候,由於受到庫設計的一些限制,我們不得不使用Java 1.0的IO流類。特別要指出的是,在舊流庫的基礎上新加了java.util.zip庫,它們依賴舊的流組件。所以最明智的做法是“嘗試性”地使用ReaderWriter類。若代碼不能通過編譯,便知道必須換回老式庫。

下面這張表格分舊庫與新庫分別總結了信息發起與接收之間的對應關係。

Sources & Sinks:
Java 1.0 class

Corresponding Java 1.1 class

InputStream

Reader
converter: InputStreamReader

OutputStream

Writer
converter: OutputStreamWriter

FileInputStream

FileReader

FileOutputStream

FileWriter

StringBufferInputStream

StringReader

(no corresponding class)

StringWriter

ByteArrayInputStream

CharArrayReader

ByteArrayOutputStream

CharArrayWriter

PipedInputStream

PipedReader

PipedOutputStream

PipedWriter

我們發現即使不完全一致,但舊庫組件中的接口與新接口通常也是類似的。

10.7.2 修改數據流的行為

在Java 1.0中,數據流通過FilterInputStreamFilterOutputStream的“裝飾器”(Decorator)子類適應特定的需求。Java 1.1的IO流沿用了這一思想,但沒有繼續採用所有裝飾器都從相同filter(過濾器)基類中派生這一做法。若通過觀察類的層次結構來理解它,這可能令人出現少許的困惑。

在下面這張表格中,對應關係比上一張表要粗糙一些。之所以會出現這個差別,是由類的組織造成的:儘管BufferedOutputStreamFilterOutputStream的一個子類,但是BufferedWriter並不是FilterWriter的子類(對後者來說,儘管它是一個抽象類,但沒有自己的子類或者近似子類的東西,也沒有一個“佔位符”可用,所以不必費心地尋找)。然而,兩個類的接口是非常相似的,而且不管在什麼情況下,顯然應該儘可能地使用新版本,而不應考慮舊版本(也就是說,除非在一些類中必須生成一個Stream,不可生成Reader或者Writer)。

Filters:
Java 1.0 class

Corresponding Java 1.1 class

FilterInputStream

FilterReader

FilterOutputStream

FilterWriter (abstract class with no subclasses)

BufferedInputStream

BufferedReader
(also has readLine( ))

BufferedOutputStream

BufferedWriter

DataInputStream

use DataInputStream
(Except when you need to use readLine( ), when you should use a BufferedReader)

PrintStream

PrintWriter

LineNumberInputStream

LineNumberReader

StreamTokenizer

StreamTokenizer
(use constructor that takes a Reader instead)

PushBackInputStream

PushBackReader

過濾器:Java 1.0類 對應的Java 1.1類

FilterInputStream FilterReader
FilterOutputStream FilterWriter(沒有子類的抽象類)
BufferedInputStream BufferedReader(也有readLine())
BufferedOutputStream BufferedWriter
DataInputStream 使用DataInputStream(除非要使用readLine(),那時需要使用一個BufferedReader)
PrintStream PrintWriter
LineNumberInputStream LineNumberReader
StreamTokenizer StreamTokenizer(用構造器取代Reader)
PushBackInputStream PushBackReader

有一條規律是顯然的:若想使用readLine(),就不要再用一個DataInputStream來實現(否則會在編譯期得到一條出錯消息),而應使用一個BufferedReader。但除這種情況以外,DataInputStream仍是Java 1.1 IO庫的“首選”成員。

為了將向PrintWriter的過渡變得更加自然,它提供了能採用任何OutputStream對象的構造器。PrintWriter提供的格式化支持沒有PrintStream那麼多;但接口幾乎是相同的。

10.7.3 未改變的類

顯然,Java庫的設計人員覺得以前的一些類毫無問題,所以沒有對它們作任何修改,可象以前那樣繼續使用它們:

沒有對應Java 1.1類的Java 1.0類

DataOutputStream
File
RandomAccessFile
SequenceInputStream

特別未加改動的是DataOutputStream,所以為了用一種可轉移的格式保存和獲取數據,必須沿用InputStreamOutputStream層次結構。

10.7.4 一個例子

為體驗新類的效果,下面讓我們看看如何修改IOStreamDemo.java示例的相應區域,以便使用ReaderWriter類:

//: NewIODemo.java
// Java 1.1 IO typical usage
import java.io.*;

public class NewIODemo {
  public static void main(String[] args) {
    try {
      // 1. Reading input by lines:
      BufferedReader in =
        new BufferedReader(
          new FileReader(args[0]));
      String s, s2 = new String();
      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(
            // Oops: must use deprecated class:
            new StringBufferInputStream(s2));
        while(true)
          System.out.print((char)in3.readByte());
      } catch(EOFException e) {
        System.out.println("End of stream");
      }

      // 4. Line numbering & file output
      try {
        LineNumberReader li =
          new LineNumberReader(
            new StringReader(s2));
        BufferedReader in4 =
          new BufferedReader(li);
        PrintWriter out1 =
          new PrintWriter(
            new BufferedWriter(
              new FileWriter("IODemo.out")));
        while((s = in4.readLine()) != null )
          out1.println(
            "Line " + li.getLineNumber() + s);
        out1.close();
      } catch(EOFException e) {
        System.out.println("End of stream");
      }

      // 5. Storing & recovering data
      try {
        DataOutputStream out2 =
          new DataOutputStream(
            new BufferedOutputStream(
              new FileOutputStream("Data.txt")));
        out2.writeDouble(3.14159);
        out2.writeBytes("That was pi");
        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());
      } catch(EOFException e) {
        System.out.println("End of stream");
      }

      // 6. Reading and writing random access
      // files is the same as before.
      // (not repeated here)

    } catch(FileNotFoundException e) {
      System.out.println(
        "File Not Found:" + args[1]);
    } catch(IOException e) {
      System.out.println("IO Exception");
    }
  }
} ///:~

大家一般看見的是轉換過程非常直觀,代碼看起來也頗相似。但這些都不是重要的區別。最重要的是,由於隨機訪問文件已經改變,所以第6節未再重複。

第1節收縮了一點兒,因為假如要做的全部事情就是讀取行輸入,那麼只需要將一個FileReader封裝到BufferedReader之內即可。第1b節展示了封裝System.in,以便讀取控制檯輸入的新方法。這裡的代碼量增多了一些,因為System.in是一個DataInputStream,而且BufferedReader需要一個Reader參數,所以要用InputStreamReader來進行轉換。

在2節,可以看到如果有一個字符串,而且想從中讀取數據,只需用一個StringReader替換StringBufferInputStream,剩下的代碼是完全相同的。

第3節揭示了新IO流庫設計中的一個錯誤。如果有一個字符串,而且想從中讀取數據,那麼不能再以任何形式使用StringBufferInputStream。若編譯一個涉及StringBufferInputStream的代碼,會得到一條“反對”消息,告訴我們不要用它。此時最好換用一個StringReader。但是,假如要象第3節這樣進行格式化的內存輸入,就必須使用DataInputStream——沒有什麼DataReader可以代替它——而DataInputStream很不幸地要求用到一個InputStream參數。所以我們沒有選擇的餘地,只好使用編譯器不贊成的StringBufferInputStream類。編譯器同樣會發出反對信息,但我們對此束手無策(註釋②)。 StringReader替換StringBufferInputStream,剩下的代碼是完全相同的。

②:到你現在正式使用的時候,這個錯誤可能已經修正。

第4節明顯是從老式數據流到新數據流的一個直接轉換,沒有需要特別指出的。在第5節中,我們被強迫使用所有的老式數據流,因為DataOutputStreamDataInputStream要求用到它們,而且沒有可供替換的東西。然而,編譯期間不會產生任何“反對”信息。若不贊成一種數據流,通常是由於它的構造器產生了一條反對消息,禁止我們使用整個類。但在DataInputStream的情況下,只有readLine()是不贊成使用的,因為我們最好為readLine()使用一個BufferedReader(但為其他所有格式化輸入都使用一個DataInputStream)。

若比較第5節和IOStreamDemo.java中的那一小節,會注意到在這個版本中,數據是在文本之前寫入的。那是由於Java 1.1本身存在一個錯誤,如下述代碼所示:

//: IOBug.java
// Java 1.1 (and higher?) IO Bug
import java.io.*;

public class IOBug {
  public static void main(String[] args)
  throws Exception {
    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());
  }
} ///:~

看起來,我們在對一個writeBytes()的調用之後寫入的任何東西都不是能夠恢復的。這是一個十分有限的錯誤,希望在你讀到本書的時候已獲得改正。為檢測是否改正,請運行上述程序。若沒有得到一個異常,而且值都能正確打印出來,就表明已經改正。

10.7.5 重導向標準IO

Java 1.1在System類中添加了特殊的方法,允許我們重新定向標準輸入、輸出以及錯誤IO流。此時要用到下述簡單的靜態方法調用:

setIn(InputStream)
setOut(PrintStream)
setErr(PrintStream)

如果突然要在屏幕上生成大量輸出,而且滾動的速度快於人們的閱讀速度,輸出的重定向就顯得特別有用。在一個命令行程序中,如果想重複測試一個特定的用戶輸入序列,輸入的重定向也顯得特別有價值。下面這個簡單的例子展示了這些方法的使用:

//: Redirecting.java
// Demonstrates the use of redirection for
// standard IO in Java 1.1
import java.io.*;

class Redirecting {
  public static void main(String[] args) {
    try {
      BufferedInputStream in =
        new BufferedInputStream(
          new FileInputStream(
            "Redirecting.java"));
      // Produces deprecation message:
      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()) != null)
        System.out.println(s);
      out.close(); // Remember this!
    } catch(IOException e) {
      e.printStackTrace();
    }
  }
} ///:~

這個程序的作用是將標準輸入同一個文件連接起來,並將標準輸出和錯誤重定向至另一個文件。 這是不可避免會遇到“反對”消息的另一個例子。用-deprecation標誌編譯時得到的消息如下:

Note:The constructor java.io.PrintStream(java.io.OutputStream) has been deprecated. 注意:不推薦使用構造器java.io.PrintStream(java.io.OutputStream)

然而,無論System.setOut()還是System.setErr()都要求用一個PrintStream作為參數使用,所以必須調用PrintStream構造器。所以大家可能會覺得奇怪,既然Java 1.1通過反對構造器而反對了整個PrintStream,為什麼庫的設計人員在添加這個反對的同時,依然為System添加了新方法,且指明要求用PrintStream,而不是用PrintWriter呢?畢竟,後者是一個嶄新和首選的替換措施呀?這真令人費解。

10.8 壓縮

Java 1.1也添加一個類,用以支持對壓縮格式的數據流的讀寫。它們封裝到現成的IO類中,以提供壓縮功能。

此時Java 1.1的一個問題顯得非常突出:它們不是從新的ReaderWriter類派生出來的,而是屬於InputStreamOutputStream層次結構的一部分。所以有時不得不混合使用兩種類型的數據流(注意可用InputStreamReaderOutputStreamWriter在不同的類型間方便地進行轉換)。

Java 1.1壓縮類 功能
CheckedInputStream GetCheckSum()為任何InputStream產生校驗和(不僅是解壓)
CheckedOutputStream GetCheckSum()為任何OutputStream產生校驗和(不僅是解壓)
DeflaterOutputStream 用於壓縮類的基類
ZipOutputStream 一個DeflaterOutputStream,將數據壓縮成Zip文件格式
GZIPOutputStream 一個DeflaterOutputStream,將數據壓縮成GZIP文件格式
InflaterInputStream 用於解壓類的基類
ZipInputStream 一個DeflaterInputStream,解壓用Zip文件格式保存的數據
GZIPInputStream 一個DeflaterInputStream,解壓用GZIP文件格式保存的數據

儘管存在許多種壓縮算法,但是Zip和GZIP可能最常用的。所以能夠很方便地用多種現成的工具來讀寫這些格式的壓縮數據。

10.8.1 用GZIP進行簡單壓縮

GZIP接口非常簡單,所以如果只有單個數據流需要壓縮(而不是一系列不同的數據),那麼它就可能是最適當選擇。下面是對單個文件進行壓縮的例子:

//: GZIPcompress.java
// Uses Java 1.1 GZIP compression to compress
// a file whose name is passed on the command
// line.
import java.io.*;
import java.util.zip.*;

public class GZIPcompress {
  public static void main(String[] args) {
    try {
      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();
      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);
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

壓縮類的用法非常直觀——只需將輸出流封裝到一個GZIPOutputStream或者ZipOutputStream內,並將輸入流封裝到GZIPInputStream或者ZipInputStream內即可。剩餘的全部操作就是標準的IO讀寫。然而,這是一個很典型的例子,我們不得不混合使用新舊IO流:數據的輸入使用Reader類,而GZIPOutputStream的構造器只能接收一個OutputStream對象,不能接收Writer對象。

10.8.2 用Zip進行多文件保存

提供了Zip支持的Java 1.1庫顯得更加全面。利用它可以方便地保存多個文件。甚至有一個獨立的類來簡化對Zip文件的讀操作。這個庫采采用的是標準Zip格式,所以能與當前因特網上使用的大量壓縮、解壓工具很好地協作。下面這個例子採取了與前例相同的形式,但能根據我們需要控制任意數量的命令行參數。除此之外,它展示瞭如何用Checksum類來計算和校驗文件的“校驗和”(Checksum)。可選用兩種類型的ChecksumAdler32(速度要快一些)和CRC32(慢一些,但更準確)。

//: ZipCompress.java
// Uses Java 1.1 Zip compression to compress
// any number of files whose names are passed
// on the command line.
import java.io.*;
import java.util.*;
import java.util.zip.*;

public class ZipCompress {
  public static void main(String[] args) {
    try {
      FileOutputStream f =
        new FileOutputStream("test.zip");
      CheckedOutputStream csum =
        new CheckedOutputStream(
          f, new Adler32());
      ZipOutputStream out =
        new ZipOutputStream(
          new BufferedOutputStream(csum));
      out.setComment("A test of Java Zipping");
      // Can't read the above comment, though
      for(int i = 0; i < args.length; i++) {
        System.out.println(
          "Writing file " + args[i]);
        BufferedReader in =
          new BufferedReader(
            new FileReader(args[i]));
        out.putNextEntry(new ZipEntry(args[i]));
        int c;
        while((c = in.read()) != -1)
          out.write(c);
        in.close();
      }
      out.close();
      // Checksum valid only after the file
      // has been closed!
      System.out.println("Checksum: " +
        csum.getChecksum().getValue());
      // Now extract the files:
      System.out.println("Reading file");
      FileInputStream fi =
         new FileInputStream("test.zip");
      CheckedInputStream csumi =
        new CheckedInputStream(
          fi, new Adler32());
      ZipInputStream in2 =
        new ZipInputStream(
          new BufferedInputStream(csumi));
      ZipEntry ze;
      System.out.println("Checksum: " +
        csumi.getChecksum().getValue());
      while((ze = in2.getNextEntry()) != null) {
        System.out.println("Reading file " + ze);
        int x;
        while((x = in2.read()) != -1)
          System.out.write(x);
      }
      in2.close();
      // Alternative way to open and read
      // zip files:
      ZipFile zf = new ZipFile("test.zip");
      Enumeration e = zf.entries();
      while(e.hasMoreElements()) {
        ZipEntry ze2 = (ZipEntry)e.nextElement();
        System.out.println("File: " + ze2);
        // ... and extract the data as before
      }
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

對於要加入壓縮檔的每一個文件,都必須調用putNextEntry(),並將其傳遞給一個ZipEntry對象。ZipEntry對象包含了一個功能全面的接口,利用它可以獲取和設置Zip文件內那個特定的Entry(入口)上能夠接受的所有數據:名字、壓縮後和壓縮前的長度、日期、CRC校驗和、額外字段的數據、註釋、壓縮方法以及它是否一個目錄入口等等。然而,雖然Zip格式提供了設置密碼的方法,但Java的Zip庫沒有提供這方面的支持。而且儘管CheckedInputStreamCheckedOutputStream同時提供了對Adler32CRC32校驗和的支持,但是ZipEntry只支持CRC的接口。這雖然屬於基層Zip格式的限制,但卻限制了我們使用速度更快的Adler32

為解壓文件,ZipInputStream提供了一個getNextEntry()方法,能在有的前提下返回下一個ZipEntry。作為一個更簡潔的方法,可以用ZipFile對象讀取文件。該對象有一個entries()方法,可以為ZipEntry返回一個Enumeration(枚舉)。

為讀取校驗和,必須多少擁有對關聯的Checksum對象的訪問權限。在這裡保留了指向CheckedOutputStreamCheckedInputStream對象的一個引用。但是,也可以只佔有指向Checksum對象的一個引用。

Zip流中一個令人困惑的方法是setComment()。正如前面展示的那樣,我們可在寫一個文件時設置註釋內容,但卻沒有辦法取出ZipInputStream內的註釋。看起來,似乎只能通過ZipEntry逐個入口地提供對註釋的完全支持。

當然,使用GZIP或Zip庫時並不僅僅限於文件——可以壓縮任何東西,包括要通過網絡連接發送的數據。

10.8.3 Java歸檔(jar)實用程序

Zip格式亦在Java 1.1的JAR(Java ARchive)文件格式中得到了採用。這種文件格式的作用是將一系列文件合併到單個壓縮文件裡,就象Zip那樣。然而,同Java中其他任何東西一樣,JAR文件是跨平臺的,所以不必關心涉及具體平臺的問題。除了可以包括聲音和圖像文件以外,也可以在其中包括類文件。

涉及因特網應用時,JAR文件顯得特別有用。在JAR文件之前,Web瀏覽器必須重複多次請求Web服務器,以便下載完構成一個“程序片”(Applet)的所有文件。除此以外,每個文件都是未經壓縮的。但在將所有這些文件合併到一個JAR文件裡以後,只需向遠程服務器發出一次請求即可。同時,由於採用了壓縮技術,所以可在更短的時間裡獲得全部數據。另外,JAR文件裡的每個入口(條目)都可以加上數字化簽名(詳情參考Java用戶文檔)。

一個JAR文件由一系列採用Zip壓縮格式的文件構成,同時還有一張“詳情單”,對所有這些文件進行了描述(可創建自己的詳情單文件;否則,jar程序會為我們代勞)。在聯機用戶文檔中,可以找到與JAR詳情單更多的資料(詳情單的英語是“Manifest”)。 jar實用程序已與Sun的JDK配套提供,可以按我們的選擇自動壓縮文件。請在命令行調用它:

jar [選項] 說明 [詳情單] 輸入文件

其中,“選項”用一系列字母表示(不必輸入連字號或其他任何指示符)。如下所示:

c 創建新的或空的壓縮檔
t 列出目錄表
x 解壓所有文件
x file 解壓指定文件
f 指出“我準備向你提供文件名”。若省略此參數,jar會假定它的輸入來自標準輸入;或者在它創建文件時,輸出會進入標準輸出內
m 指出第一個參數將是用戶自建的詳情表文件的名字
v 產生詳細輸出,對jar做的工作進行鉅細無遺的描述
O 只保存文件;不壓縮文件(用於創建一個JAR文件,以便我們將其置入自己的類路徑中)
M 不自動生成詳情表文件

在準備進入JAR文件的文件中,若包括了一個子目錄,那個子目錄會自動添加,其中包括它自己的所有子目錄,以此類推。路徑信息也會得到保留。

下面是調用jar的一些典型方法:

jar cf myJarFile.jar *.class

用於創建一個名為myJarFile.jar的JAR文件,其中包含了當前目錄中的所有類文件,同時還有自動產生的詳情表文件。

jar cmf myJarFile.jar myManifestFile.mf *.class

與前例類似,但添加了一個名為myManifestFile.mf的用戶自建詳情表文件。

jar tf myJarFile.jar

生成myJarFile.jar內所有文件的一個目錄表。

jar tvf myJarFile.jar

添加verbose(詳盡)標誌,提供與myJarFile.jar中的文件有關的、更詳細的資料。

jar cvf myApp.jar audio classes image

假定audioclassesimage是子目錄,這樣便將所有子目錄合併到文件myApp.jar中。其中也包括了verbose標誌,可在jar程序工作時反饋更詳盡的信息。

如果用O選項創建了一個JAR文件,那個文件就可置入自己的類路徑(CLASSPATH)中:

CLASSPATH="lib1.jar;lib2.jar;"

Java能在lib1.jarlib2.jar中搜索目標類文件。

jar工具的功能沒有zip工具那麼豐富。例如,不能夠添加或更新一個現成JAR文件中的文件,只能從頭開始新建一個JAR文件。此外,不能將文件移入一個JAR文件,並在移動後將它們刪除。然而,在一種平臺上創建的JAR文件可在其他任何平臺上由jar工具毫無阻礙地讀出(這個問題有時會困擾zip工具)。

正如大家在第13章會看到的那樣,我們也用JAR為Java Beans打包。

10.9 對象序列化

Java 1.1增添了一種有趣的特性,名為“對象序列化”(Object Serialization)。它面向那些實現了Serializable接口的對象,可將它們轉換成一系列字節,並可在以後完全恢復回原來的樣子。這一過程亦可通過網絡進行。這意味著序列化機制能自動補償操作系統間的差異。換句話說,可以先在Windows機器上創建一個對象,對其序列化,然後通過網絡發給一臺Unix機器,然後在那裡準確無誤地重新“裝配”。不必關心數據在不同機器上如何表示,也不必關心字節的順序或者其他任何細節。

就其本身來說,對象的序列化是非常有趣的,因為利用它可以實現“有限持久化”。請記住“持久化”意味著對象的“生存時間”並不取決於程序是否正在執行——它存在或“生存”於程序的每一次調用之間。通過序列化一個對象,將其寫入磁盤,以後在程序重新調用時重新恢復那個對象,就能圓滿實現一種“持久”效果。之所以稱其為“有限”,是因為不能用某種persistent(持久)關鍵字簡單地地定義一個對象,並讓系統自動照看其他所有細節問題(儘管將來可能成為現實)。相反,必須在自己的程序中明確地序列化和組裝對象。

語言裡增加了對象序列化的概念後,可提供對兩種主要特性的支持。Java 1.1的“遠程方法調用”(RMI)使本來存在於其他機器的對象可以表現出好象就在本地機器上的行為。將消息發給遠程對象時,需要通過對象序列化來傳輸參數和返回值。RMI將在第15章作具體討論。

對象的序列化也是Java Beans必需的,後者由Java 1.1引入。使用一個Bean時,它的狀態信息通常在設計期間配置好。程序啟動以後,這種狀態信息必須保存下來,以便程序啟動以後恢復;具體工作由對象序列化完成。

對象的序列化處理非常簡單,只需對象實現了Serializable接口即可(該接口僅是一個標記,沒有方法)。在Java 1.1中,許多標準庫類都發生了改變,以便能夠序列化——其中包括用於基本數據類型的全部包裝器、所有集合類以及其他許多東西。甚至Class對象也可以序列化(第11章講述了具體實現過程)。

為序列化一個對象,首先要創建某些OutputStream對象,然後將其封裝到ObjectOutputStream對象內。此時,只需調用writeObject()即可完成對象的序列化,並將其發送給OutputStream。相反的過程是將一個InputStream封裝到ObjectInputStream內,然後調用readObject()。和往常一樣,我們最後獲得的是指向一個向上轉換Object的引用,所以必須向下轉換,以便能夠直接設置。

對象序列化特別“聰明”的一個地方是它不僅保存了對象的“全景圖”,而且能追蹤對象內包含的所有引用並保存那些對象;接著又能對每個對象內包含的引用進行追蹤;以此類推。我們有時將這種情況稱為“對象網”,單個對象可與之建立連接。而且它還包含了對象的引用數組以及成員對象。若必須自行操縱一套對象序列化機制,那麼在代碼裡追蹤所有這些鏈接時可能會顯得非常麻煩。在另一方面,由於Java對象的序列化似乎找不出什麼缺點,所以請儘量不要自己動手,讓它用優化的算法自動維護整個對象網。下面這個例子對序列化機制進行了測試。它建立了許多鏈接對象的一個Worm(蠕蟲),每個對象都與Worm中的下一段鏈接,同時又與屬於不同類(Data)的對象引用數組鏈接:

//: Worm.java
// Demonstrates object serialization in Java 1.1
import java.io.*;

class Data implements Serializable {
  private int i;
  Data(int x) { i = x; }
  public String toString() {
    return Integer.toString(i);
  }
}

public class Worm implements Serializable {
  // Generate a random int value:
  private static int r() {
    return (int)(Math.random() * 10);
  }
  private Data[] d = {
    new Data(r()), new Data(r()), new Data(r())
  };
  private Worm next;
  private char c;
  // Value of i == number of segments
  Worm(int i, char x) {
    System.out.println(" Worm constructor: " + i);
    c = x;
    if(--i > 0)
      next = new Worm(i, (char)(x + 1));
  }
  Worm() {
    System.out.println("Default constructor");
  }
  public String toString() {
    String s = ":" + c + "(";
    for(int i = 0; i < d.length; i++)
      s += d[i].toString();
    s += ")";
    if(next != null)
      s += next.toString();
    return s;
  }
  public static void main(String[] args) {
    Worm w = new Worm(6, 'a');
    System.out.println("w = " + w);
    try {
      ObjectOutputStream out =
        new ObjectOutputStream(
          new FileOutputStream("worm.out"));
      out.writeObject("Worm storage");
      out.writeObject(w);
      out.close(); // Also flushes output
      ObjectInputStream in =
        new ObjectInputStream(
          new FileInputStream("worm.out"));
      String s = (String)in.readObject();
      Worm w2 = (Worm)in.readObject();
      System.out.println(s + ", w2 = " + w2);
    } catch(Exception e) {
      e.printStackTrace();
    }
    try {
      ByteArrayOutputStream bout =
        new ByteArrayOutputStream();
      ObjectOutputStream out =
        new ObjectOutputStream(bout);
      out.writeObject("Worm storage");
      out.writeObject(w);
      out.flush();
      ObjectInputStream in =
        new ObjectInputStream(
          new ByteArrayInputStream(
            bout.toByteArray()));
      String s = (String)in.readObject();
      Worm w3 = (Worm)in.readObject();
      System.out.println(s + ", w3 = " + w3);
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

更有趣的是,Worm內的Data對象數組是用隨機數字初始化的(這樣便不用懷疑編譯器保留了某種原始信息)。每個Worm段都用一個Char標記。這個Char是在重複生成鏈接的Worm列表時自動產生的。創建一個Worm時,需告訴構造器希望它有多長。為產生下一個引用(next),它總是用減去1的長度來調用Worm構造器。最後一個next引用則保持為null(空),表示已抵達Worm的尾部。

上面的所有操作都是為了加深事情的複雜程度,加大對象序列化的難度。然而,真正的序列化過程卻是非常簡單的。一旦從另外某個流裡創建了ObjectOutputStreamwriteObject()就會序列化對象。注意也可以為一個String調用writeObject()。亦可使用與DataOutputStream相同的方法寫入所有基本數據類型(它們有相同的接口)。

有兩個單獨的try塊看起來是類似的。第一個讀寫的是文件,而另一個讀寫的是一個ByteArray(字節數組)。可利用對任何DataInputStream或者DataOutputStream的序列化來讀寫特定的對象;正如在關於連網的那一章會講到的那樣,這些對象甚至包括網絡。一次循環後的輸出結果如下:

Worm constructor: 6
Worm constructor: 5
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worm constructor: 1
w = :a(262):b(100):c(396):d(480):e(316):f(398)
Worm storage, w2 = :a(262):b(100):c(396):d(480):e(316):f(398)
Worm storage, w3 = :a(262):b(100):c(396):d(480):e(316):f(398)

可以看出,裝配回原狀的對象確實包含了原來那個對象裡包含的所有鏈接。

注意在對一個Serializable(可序列化)對象進行重新裝配的過程中,不會調用任何構造器(甚至默認構造器)。整個對象都是通過從InputStream中取得數據恢復的。

作為Java 1.1特性的一種,我們注意到對象的序列化並不屬於新的ReaderWriter層次結構的一部分,而是沿用老式的InputStreamOutputStream結構。所以在一些特殊的場合下,不得不混合使用兩種類型的層次結構。

10.9.1 尋找類

讀者或許會奇怪為什麼需要一個對象從它的序列化狀態中恢復。舉個例子來說,假定我們序列化一個對象,並通過網絡將其作為文件傳送給另一臺機器。此時,位於另一臺機器的程序可以只用文件目錄來重新構造這個對象嗎?

回答這個問題的最好方法就是做一個實驗。下面這個文件位於本章的子目錄下:

//: Alien.java
// A serializable class
import java.io.*;

public class Alien implements Serializable {
} ///:~

用於創建和序列化一個Alien對象的文件位於相同的目錄下:

//: FreezeAlien.java
// Create a serialized output file
import java.io.*;

public class FreezeAlien {
  public static void main(String[] args)
      throws Exception {
    ObjectOutput out =
      new ObjectOutputStream(
        new FileOutputStream("file.x"));
    Alien zorcon = new Alien();
    out.writeObject(zorcon);
  }
} ///:~

該程序並不是捕獲和控制異常,而是將異常簡單、直接地傳遞到main()外部,這樣便能在命令行報告它們。

程序編譯並運行後,將結果產生的file.x複製到名為xfiles的子目錄,代碼如下:

//: ThawAlien.java
// Try to recover a serialized file without the
// class of object that's stored in that file.
package c10.xfiles;
import java.io.*;

public class ThawAlien {
  public static void main(String[] args)
      throws Exception {
    ObjectInputStream in =
      new ObjectInputStream(
        new FileInputStream("file.x"));
    Object mystery = in.readObject();
    System.out.println(
      mystery.getClass().toString());
  }
} ///:~

該程序能打開文件,併成功讀取mystery對象中的內容。然而,一旦嘗試查找與對象有關的任何資料——這要求AlienClass對象——Java虛擬機(JVM)便找不到Alien.class(除非它正好在類路徑內,而本例理應相反)。這樣就會得到一個名叫ClassNotFoundException的異常(同樣地,若非能夠校驗Alien存在的證據,否則它等於消失)。

恢復了一個序列化的對象後,如果想對其做更多的事情,必須保證JVM能在本地類路徑或者因特網的其他什麼地方找到相關的.class文件。

10.9.2 序列化的控制

正如大家看到的那樣,默認的序列化機制並不難操縱。然而,假若有特殊要求又該怎麼辦呢?我們可能有特殊的安全問題,不希望對象的某一部分序列化;或者某一個子對象完全不必序列化,因為對象恢復以後,那一部分需要重新創建。

此時,通過實現Externalizable接口,用它代替Serializable接口,便可控制序列化的具體過程。這個Externalizable接口擴展了Serializable,並增添了兩個方法:writeExternal()readExternal()。在序列化和重新裝配的過程中,會自動調用這兩個方法,以便我們執行一些特殊操作。

下面這個例子展示了Externalizable接口方法的簡單應用。注意Blip1Blip2幾乎完全一致,除了極微小的差別(自己研究一下代碼,看看是否能發現):

//: Blips.java
// Simple use of Externalizable & a pitfall
import java.io.*;
import java.util.*;

class Blip1 implements Externalizable {
  public Blip1() {
    System.out.println("Blip1 Constructor");
  }
  public void writeExternal(ObjectOutput out)
      throws IOException {
    System.out.println("Blip1.writeExternal");
  }
  public void readExternal(ObjectInput in)
     throws IOException, ClassNotFoundException {
    System.out.println("Blip1.readExternal");
  }
}

class Blip2 implements Externalizable {
  Blip2() {
    System.out.println("Blip2 Constructor");
  }
  public void writeExternal(ObjectOutput out)
      throws IOException {
    System.out.println("Blip2.writeExternal");
  }
  public void readExternal(ObjectInput in)
     throws IOException, ClassNotFoundException {
    System.out.println("Blip2.readExternal");
  }
}

public class Blips {
  public static void main(String[] args) {
    System.out.println("Constructing objects:");
    Blip1 b1 = new Blip1();
    Blip2 b2 = new Blip2();
    try {
      ObjectOutputStream o =
        new ObjectOutputStream(
          new FileOutputStream("Blips.out"));
      System.out.println("Saving objects:");
      o.writeObject(b1);
      o.writeObject(b2);
      o.close();
      // Now get them back:
      ObjectInputStream in =
        new ObjectInputStream(
          new FileInputStream("Blips.out"));
      System.out.println("Recovering b1:");
      b1 = (Blip1)in.readObject();
      // OOPS! Throws an exception:
//!   System.out.println("Recovering b2:");
//!   b2 = (Blip2)in.readObject();
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

該程序輸出如下:

Constructing objects:
Blip1 Constructor
Blip2 Constructor
Saving objects:
Blip1.writeExternal
Blip2.writeExternal
Recovering b1:
Blip1 Constructor
Blip1.readExternal

未恢復Blip2對象的原因是那樣做會導致一個異常。你找出了Blip1Blip2之間的區別嗎?Blip1的構造器是“公共的”(public),Blip2的構造器則不然,這樣便會在恢復時造成異常。試試將Blip2的構造器屬性變成public,然後刪除//!註釋標記,看看是否能得到正確的結果。

恢復b1後,會調用Blip1默認構造器。這與恢復一個Serializable(可序列化)對象不同。在後者的情況下,對象完全以它保存下來的二進制位為基礎恢復,不存在構造器調用。而對一個Externalizable對象,所有普通的默認構建行為都會發生(包括在字段定義時的初始化),而且會調用readExternal()。必須注意這一事實——特別注意所有默認的構建行為都會進行——否則很難在自己的Externalizable對象中產生正確的行為。

下面這個例子揭示了保存和恢復一個Externalizable對象必須做的全部事情:

//: Blip3.java
// Reconstructing an externalizable object
import java.io.*;
import java.util.*;

class Blip3 implements Externalizable {
  int i;
  String s; // No initialization
  public Blip3() {
    System.out.println("Blip3 Constructor");
    // s, i not initialized
  }
  public Blip3(String x, int a) {
    System.out.println("Blip3(String x, int a)");
    s = x;
    i = a;
    // s & i initialized only in non-default
    // constructor.
  }
  public String toString() { return s + i; }
  public void writeExternal(ObjectOutput out)
      throws IOException {
    System.out.println("Blip3.writeExternal");
    // You must do this:
    out.writeObject(s); out.writeInt(i);
  }
  public void readExternal(ObjectInput in)
     throws IOException, ClassNotFoundException {
    System.out.println("Blip3.readExternal");
    // You must do this:
    s = (String)in.readObject();
    i =in.readInt();
  }
  public static void main(String[] args) {
    System.out.println("Constructing objects:");
    Blip3 b3 = new Blip3("A String ", 47);
    System.out.println(b3.toString());
    try {
      ObjectOutputStream o =
        new ObjectOutputStream(
          new FileOutputStream("Blip3.out"));
      System.out.println("Saving object:");
      o.writeObject(b3);
      o.close();
      // Now get it back:
      ObjectInputStream in =
        new ObjectInputStream(
          new FileInputStream("Blip3.out"));
      System.out.println("Recovering b3:");
      b3 = (Blip3)in.readObject();
      System.out.println(b3.toString());
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

其中,字段si只在第二個構造器中初始化,不關默認構造器的事。這意味著假如不在readExternal中初始化si,它們就會成為null(因為在對象創建的第一步中已將對象的存儲空間清除為1)。若註釋掉跟隨於"You must do this"後面的兩行代碼,並運行程序,就會發現當對象恢復以後,snull,而i是零。

若從一個Externalizable對象繼承,通常需要調用writeExternal()readExternal()的基類版本,以便正確地保存和恢復基類組件。

所以為了讓一切正常運作起來,千萬不可僅在writeExternal()方法執行期間寫入對象的重要數據(沒有默認的行為可用來為一個Externalizable對象寫入所有成員對象)的,而是必須在readExternal()方法中也恢復那些數據。初次操作時可能會有些不習慣,因為Externalizable對象的默認構建行為使其看起來似乎正在進行某種存儲與恢復操作。但實情並非如此。

(1) transient(臨時)關鍵字

控制序列化過程時,可能有一個特定的子對象不願讓Java的序列化機制自動保存與恢復。一般地,若那個子對象包含了不想序列化的敏感信息(如密碼),就會面臨這種情況。即使那種信息在對象中具有private(私有)屬性,但一旦經序列化處理,人們就可以通過讀取一個文件,或者攔截網絡傳輸得到它。

為防止對象的敏感部分被序列化,一個辦法是將自己的類實現為Externalizable,就象前面展示的那樣。這樣一來,沒有任何東西可以自動序列化,只能在writeExternal()明確序列化那些需要的部分。

然而,若操作的是一個Serializable對象,所有序列化操作都會自動進行。為解決這個問題,可以用transient(臨時)逐個字段地關閉序列化,它的意思是“不要麻煩你(指自動機制)保存或恢復它了——我會自己處理的”。

例如,假設一個Login對象包含了與一個特定的登錄會話有關的信息。校驗登錄的合法性時,一般都想將數據保存下來,但不包括密碼。為做到這一點,最簡單的辦法是實現Serializable,並將password字段設為transient。下面是具體的代碼:

//: Logon.java
// Demonstrates the "transient" keyword
import java.io.*;
import java.util.*;

class Logon implements Serializable {
  private Date date = new Date();
  private String username;
  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.toString() +
      "\n   password: " + pwd;
  }
  public static void main(String[] args) {
    Logon a = new Logon("Hulk", "myLittlePony");
    System.out.println( "logon a = " + a);
    try {
      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);
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

可以看到,其中的dateusername字段保持原始狀態(未設成transient),所以會自動序列化。然而,password被設為transient,所以不會自動保存到磁盤;另外,自動序列化機制也不會作恢復它的嘗試。輸出如下:

logon a = logon info:
   username: Hulk
   date: Sun Mar 23 18:25:53 PST 1997
   password: myLittlePony
Recovering object at Sun Mar 23 18:25:59 PST 1997
logon a = logon info:
   username: Hulk
   date: Sun Mar 23 18:25:53 PST 1997
   password: (n/a)

一旦對象恢復成原來的樣子,password字段就會變成null。注意必須用toString()檢查password是否為null,因為若用重載的+運算符來裝配一個String對象,而且那個運算符遇到一個null引用,就會造成一個名為NullPointerException的異常(新版Java可能會提供避免這個問題的代碼)。

我們也發現date字段被保存到磁盤,並從磁盤恢復,沒有重新生成。

由於Externalizable對象默認時不保存它的任何字段,所以transient關鍵字只能伴隨Serializable使用。

(2) Externalizable的替代方法

若不是特別在意要實現Externalizable接口,還有另一種方法可供選用。我們可以實現Serializable接口,並添加(注意是“添加”,而非“覆蓋”或者“實現”)名為writeObject()readObject()的方法。一旦對象被序列化或者重新裝配,就會分別調用那兩個方法。也就是說,只要提供了這兩個方法,就會優先使用它們,而不考慮默認的序列化機制。 這些方法必須含有下列準確的簽名:

private void
  writeObject(ObjectOutputStream stream)
    throws IOException;

private void
  readObject(ObjectInputStream stream)
    throws IOException, ClassNotFoundException

從設計的角度出發,情況變得有些撲朔迷離。首先,大家可能認為這些方法不屬於基類或者Serializable接口的一部分,它們應該在自己的接口中得到定義。但請注意它們被定義成private,這意味著它們只能由這個類的其他成員調用。然而,我們實際並不從這個類的其他成員中調用它們,而是由ObjectOutputStreamObjectInputStreamwriteObject()readObject()方法來調用我們對象的writeObject()readObject()方法(注意我在這裡用了很大的抑制力來避免使用相同的方法名——因為怕混淆)。大家可能奇怪ObjectOutputStreamObjectInputStream如何有權訪問我們的類的private方法——只能認為這是序列化機制玩的一個把戲。

在任何情況下,接口中的定義的任何東西都會自動具有public屬性,所以假若writeObject()readObject()必須為private,那麼它們不能成為接口(interface)的一部分。但由於我們準確地加上了簽名,所以最終的效果實際與實現一個接口是相同的。

看起來似乎我們調用ObjectOutputStream.writeObject()的時候,我們傳遞給它的Serializable對象似乎會被檢查是否實現了自己的writeObject()。若答案是肯定的是,便會跳過常規的序列化過程,並調用writeObject()readObject()也會遇到同樣的情況。

還存在另一個問題。在我們的writeObject()內部,可以調用defaultWriteObject(),從而決定採取默認的writeObject()行動。類似地,在readObject()內部,可以調用defaultReadObject()。下面這個簡單的例子演示瞭如何對一個Serializable對象的存儲與恢復進行控制:

//: SerialCtl.java
// Controlling serialization by adding your own
// writeObject() and readObject() methods.
import java.io.*;

public class SerialCtl implements Serializable {
  String a;
  transient String b;
  public SerialCtl(String aa, String bb) {
    a = "Not Transient: " + aa;
    b = "Transient: " + bb;
  }
  public String toString() {
    return a + "\n" + b;
  }
  private void
    writeObject(ObjectOutputStream stream)
      throws IOException {
    stream.defaultWriteObject();
    stream.writeObject(b);
  }
  private void
    readObject(ObjectInputStream stream)
      throws IOException, ClassNotFoundException {
    stream.defaultReadObject();
    b = (String)stream.readObject();
  }
  public static void main(String[] args) {
    SerialCtl sc =
      new SerialCtl("Test1", "Test2");
    System.out.println("Before:\n" + sc);
    ByteArrayOutputStream buf =
      new ByteArrayOutputStream();
    try {
      ObjectOutputStream o =
        new ObjectOutputStream(buf);
      o.writeObject(sc);
      // Now get it back:
      ObjectInputStream in =
        new ObjectInputStream(
          new ByteArrayInputStream(
            buf.toByteArray()));
      SerialCtl sc2 = (SerialCtl)in.readObject();
      System.out.println("After:\n" + sc2);
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

在這個例子中,一個String保持原始狀態,其他設為transient(臨時),以便證明非臨時字段會被defaultWriteObject()方法自動保存,而transient字段必須在程序中明確保存和恢復。字段是在構造器內部初始化的,而不是在定義的時候,這證明瞭它們不會在重新裝配的時候被某些自動化機制初始化。

若準備通過默認機制寫入對象的非transient部分,那麼必須調用defaultWriteObject(),令其作為writeObject()中的第一個操作;並調用defaultReadObject(),令其作為readObject()的第一個操作。這些都是不常見的調用方法。舉個例子來說,當我們為一個ObjectOutputStream調用defaultWriteObject()的時候,而且沒有為其傳遞參數,就需要採取這種操作,使其知道對象的引用以及如何寫入所有非transient的部分。這種做法非常不便。

transient對象的存儲與恢復採用了我們更熟悉的代碼。現在考慮一下會發生一些什麼事情。在main()中會創建一個SerialCtl對象,隨後會序列化到一個ObjectOutputStream裡(注意這種情況下使用的是一個緩衝區,而非文件——與ObjectOutputStream完全一致)。正式的序列化操作是在下面這行代碼裡發生的:

o.writeObject(sc);

其中,writeObject()方法必須核查sc,判斷它是否有自己的writeObject()方法(不是檢查它的接口——它根本就沒有,也不是檢查類的類型,而是利用反射方法實際搜索方法)。若答案是肯定的,就使用那個方法。類似的情況也會在readObject()上發生。或許這是解決問題唯一實際的方法,但確實顯得有些古怪。

(3) 版本問題

有時候可能想改變一個可序列化的類的版本(比如原始類的對象可能保存在數據庫中)。儘管這種做法得到了支持,但一般只應在非常特殊的情況下才用它。此外,它要求操作者對背後的原理有一個比較深的認識,而我們在這裡還不想達到這種深度。JDK 1.1的HTML文檔對這一主題進行了非常全面的論述(可從Sun公司下載,但可能也成了Java開發包聯機文檔的一部分)。

10.9.3 利用“持久性”

一個比較誘人的想法是用序列化技術保存程序的一些狀態信息,從而將程序方便地恢復到以前的狀態。但在具體實現以前,有些問題是必須解決的。如果兩個對象都有指向第三個對象的引用,該如何對這兩個對象序列化呢?如果從兩個對象序列化後的狀態恢復它們,第三個對象的引用只會出現在一個對象身上嗎?如果將這兩個對象序列化成獨立的文件,然後在代碼的不同部分重新裝配它們,又會得到什麼結果呢?

下面這個例子對上述問題進行了很好的說明:

//: MyWorld.java
import java.io.*;
import java.util.*;

class House implements Serializable {}

class Animal implements Serializable {
  String name;
  House preferredHouse;
  Animal(String nm, House h) {
    name = nm;
    preferredHouse = h;
  }
  public String toString() {
    return name + "[" + super.toString() +
      "], " + preferredHouse + "\n";
  }
}

public class MyWorld {
  public static void main(String[] args) {
    House house = new House();
    Vector  animals = new Vector();
    animals.addElement(
      new Animal("Bosco the dog", house));
    animals.addElement(
      new Animal("Ralph the hamster", house));
    animals.addElement(
      new Animal("Fronk the cat", house));
    System.out.println("animals: " + animals);

    try {
      ByteArrayOutputStream buf1 =
        new ByteArrayOutputStream();
      ObjectOutputStream o1 =
        new ObjectOutputStream(buf1);
      o1.writeObject(animals);
      o1.writeObject(animals); // Write a 2nd set
      // Write to a different stream:
      ByteArrayOutputStream buf2 =
        new ByteArrayOutputStream();
      ObjectOutputStream o2 =
        new ObjectOutputStream(buf2);
      o2.writeObject(animals);
      // Now get them back:
      ObjectInputStream in1 =
        new ObjectInputStream(
          new ByteArrayInputStream(
            buf1.toByteArray()));
      ObjectInputStream in2 =
        new ObjectInputStream(
          new ByteArrayInputStream(
            buf2.toByteArray()));
      Vector animals1 = (Vector)in1.readObject();
      Vector animals2 = (Vector)in1.readObject();
      Vector animals3 = (Vector)in2.readObject();
      System.out.println("animals1: " + animals1);
      System.out.println("animals2: " + animals2);
      System.out.println("animals3: " + animals3);
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

這裡一件有趣的事情是也許是能針對一個字節數組應用對象的序列化,從而實現對任何Serializable(可序列化)對象的一個“全面複製”(全面複製意味著複製的是整個對象網,而不僅是基本對象和它的引用)。複製問題將在第12章進行全面講述。

Animal對象包含了類型為House的字段。在main()中,會創建這些Animal的一個Vector,並對其序列化兩次,分別送入兩個不同的數據流內。這些數據重新裝配並打印出來後,可看到下面這樣的結果(對象在每次運行時都會處在不同的內存位置,所以每次運行的結果有區別):

animals: [Bosco the dog[Animal@1cc76c], House@1cc769
, Ralph the hamster[Animal@1cc76d], House@1cc769
, Fronk the cat[Animal@1cc76e], House@1cc769
]
animals1: [Bosco the dog[Animal@1cca0c], House@1cca16
, Ralph the hamster[Animal@1cca17], House@1cca16
, Fronk the cat[Animal@1cca1b], House@1cca16
]
animals2: [Bosco the dog[Animal@1cca0c], House@1cca16
, Ralph the hamster[Animal@1cca17], House@1cca16
, Fronk the cat[Animal@1cca1b], House@1cca16
]
animals3: [Bosco the dog[Animal@1cca52], House@1cca5c
, Ralph the hamster[Animal@1cca5d], House@1cca5c
, Fronk the cat[Animal@1cca61], House@1cca5c
]

當然,我們希望裝配好的對象有與原來不同的地址。但注意在animals1animals2中出現了相同的地址,其中包括共享的、對House對象的引用。在另一方面,當animals3恢復以後,系統沒有辦法知道另一個流內的對象是第一個流內對象的化身,所以會產生一個完全不同的對象網。

只要將所有東西都序列化到單獨一個數據流裡,就能恢復獲得與以前寫入時完全一樣的對象網,不會不慎造成對象的重複。當然,在寫第一個和最後一個對象的時間之間,可改變對象的狀態,但那必須由我們明確採取操作——序列化時,對象會採用它們當時的任何狀態(包括它們與其他對象的連接關係)寫入。

若想保存系統狀態,最安全的做法是當作一種“微觀”操作序列化。如果序列化了某些東西,再去做其他一些工作,再來序列化更多的東西,以此類推,那麼最終將無法安全地保存系統狀態。相反,應將構成系統狀態的所有對象都置入單個集合內,並在一次操作裡完成那個集合的寫入。這樣一來,同樣只需一次方法調用,即可成功恢復之。

下面這個例子是一套假想的計算機輔助設計(CAD)系統,對這一方法進行了很好的演示。此外,它還為我們引入了static字段的問題——如留意聯機文檔,就會發現ClassSerializable(可序列化)的,所以只需簡單地序列化Class對象,就能實現static字段的保存。這無論如何都是一種明智的做法。

//: CADState.java
// Saving and restoring the state of a
// pretend CAD system.
import java.io.*;
import java.util.*;

abstract class Shape implements Serializable {
  public static final int
    RED = 1, BLUE = 2, GREEN = 3;
  private int xPos, yPos, dimension;
  private static Random r = new Random();
  private static int counter = 0;
  abstract public void setColor(int newColor);
  abstract public int getColor();
  public Shape(int xVal, int yVal, int dim) {
    xPos = xVal;
    yPos = yVal;
    dimension = dim;
  }
  public String toString() {
    return getClass().toString() +
      " color[" + getColor() +
      "] xPos[" + xPos +
      "] yPos[" + yPos +
      "] dim[" + dimension + "]\n";
  }
  public static Shape randomFactory() {
    int xVal = r.nextInt() % 100;
    int yVal = r.nextInt() % 100;
    int dim = r.nextInt() % 100;
    switch(counter++ % 3) {
      default:
      case 0: return new Circle(xVal, yVal, dim);
      case 1: return new Square(xVal, yVal, dim);
      case 2: return new Line(xVal, yVal, dim);
    }
  }
}

class Circle extends Shape {
  private static int color = RED;
  public Circle(int xVal, int yVal, int dim) {
    super(xVal, yVal, dim);
  }
  public void setColor(int newColor) {
    color = newColor;
  }
  public int getColor() {
    return color;
  }
}

class Square extends Shape {
  private static int color;
  public Square(int xVal, int yVal, int dim) {
    super(xVal, yVal, dim);
    color = RED;
  }
  public void setColor(int newColor) {
    color = newColor;
  }
  public int getColor() {
    return color;
  }
}

class Line extends Shape {
  private static int color = RED;
  public static void
  serializeStaticState(ObjectOutputStream os)
      throws IOException {
    os.writeInt(color);
  }
  public static void
  deserializeStaticState(ObjectInputStream os)
      throws IOException {
    color = os.readInt();
  }
  public Line(int xVal, int yVal, int dim) {
    super(xVal, yVal, dim);
  }
  public void setColor(int newColor) {
    color = newColor;
  }
  public int getColor() {
    return color;
  }
}

public class CADState {
  public static void main(String[] args)
      throws Exception {
    Vector shapeTypes, shapes;
    if(args.length == 0) {
      shapeTypes = new Vector();
      shapes = new Vector();
      // Add handles to the class objects:
      shapeTypes.addElement(Circle.class);
      shapeTypes.addElement(Square.class);
      shapeTypes.addElement(Line.class);
      // Make some shapes:
      for(int i = 0; i < 10; i++)
        shapes.addElement(Shape.randomFactory());
      // Set all the static colors to GREEN:
      for(int i = 0; i < 10; i++)
        ((Shape)shapes.elementAt(i))
          .setColor(Shape.GREEN);
      // Save the state vector:
      ObjectOutputStream out =
        new ObjectOutputStream(
          new FileOutputStream("CADState.out"));
      out.writeObject(shapeTypes);
      Line.serializeStaticState(out);
      out.writeObject(shapes);
    } else { // There's a command-line argument
      ObjectInputStream in =
        new ObjectInputStream(
          new FileInputStream(args[0]));
      // Read in the same order they were written:
      shapeTypes = (Vector)in.readObject();
      Line.deserializeStaticState(in);
      shapes = (Vector)in.readObject();
    }
    // Display the shapes:
    System.out.println(shapes);
  }
} ///:~

Shape(幾何形狀)類“實現了可序列化”(implements Serializable),所以從Shape繼承的任何東西也都會自動“可序列化”。每個Shape都包含了數據,而且每個派生的Shape類都包含了一個特殊的static字段,用於決定所有那些類型的Shape的顏色(如將一個static字段置入基類,結果只會產生一個字段,因為static字段未在派生類中複製)。可對基類中的方法進行覆蓋處理,以便為不同的類型設置顏色(static方法不會動態綁定,所以這些都是普通的方法)。每次調用randomFactory()方法時,它都會創建一個不同的ShapeShape值採用隨機值)。

Circle(圓)和Square(矩形)屬於對Shape的直接擴展;唯一的差別是Circle在定義時會初始化顏色,而Square在構造器中初始化。Line(直線)的問題將留到以後討論。

main()中,一個Vector用於容納Class對象,而另一個用於容納形狀。若不提供相應的命令行參數,就會創建shapeTypes Vector,並添加Class對象。然後創建shapes Vector,並添加Shape對象。接下來,所有static color值都會設成GREEN,而且所有東西都會序列化到文件CADState.out

若提供了一個命令行參數(假設CADState.out),便會打開那個文件,並用它恢復程序的狀態。無論在哪種情況下,結果產生的ShapeVector都會打印出來。下面列出它某一次運行的結果:

>java CADState
[class Circle color[3] xPos[-51] yPos[-99] dim[38]
, class Square color[3] xPos[2] yPos[61] dim[-46]
, class Line color[3] xPos[51] yPos[73] dim[64]
, class Circle color[3] xPos[-70] yPos[1] dim[16]
, class Square color[3] xPos[3] yPos[94] dim[-36]
, class Line color[3] xPos[-84] yPos[-21] dim[-35]
, class Circle color[3] xPos[-75] yPos[-43] dim[22]
, class Square color[3] xPos[81] yPos[30] dim[-45]
, class Line color[3] xPos[-29] yPos[92] dim[17]
, class Circle color[3] xPos[17] yPos[90] dim[-76]
]

>java CADState CADState.out
[class Circle color[1] xPos[-51] yPos[-99] dim[38]
, class Square color[0] xPos[2] yPos[61] dim[-46]
, class Line color[3] xPos[51] yPos[73] dim[64]
, class Circle color[1] xPos[-70] yPos[1] dim[16]
, class Square color[0] xPos[3] yPos[94] dim[-36]
, class Line color[3] xPos[-84] yPos[-21] dim[-35]
, class Circle color[1] xPos[-75] yPos[-43] dim[22]
, class Square color[0] xPos[81] yPos[30] dim[-45]
, class Line color[3] xPos[-29] yPos[92] dim[17]
, class Circle color[1] xPos[17] yPos[90] dim[-76]
]

從中可以看出,xPosyPos以及dim的值都已成功保存和恢復出來。但在獲取static信息時卻出現了問題。所有“3”都已進入,但沒有正常地出來。Circle有一個1值(定義為RED),而Square有一個0值(記住,它們是在構造器裡初始化的)。看上去似乎static根本沒有得到初始化!實情正是如此——儘管類Class是“可以序列化的”,但卻不能按我們希望的工作。所以假如想序列化static值,必須親自動手。

這正是Line中的serializeStaticState()deserializeStaticState()兩個static方法的用途。可以看到,這兩個方法都是作為存儲和恢復進程的一部分明確調用的(注意寫入序列化文件和從中讀回的順序不能改變)。所以為了使CADState.java正確運行起來,必須採用下述三種方法之一:

(1) 為幾何形狀添加一個serializeStaticState()deserializeStaticState()

(2) 刪除Vector shapeTypes以及與之有關的所有代碼

(3) 在幾何形狀內添加對新序列化和撤消序列化靜態方法的調用

要注意的另一個問題是安全,因為序列化處理也會將private數據保存下來。若有需要保密的字段,應將其標記成transient。但在這之後,必須設計一種安全的信息保存方法。這樣一來,一旦需要恢復,就可以重設那些private變量。

10.10 總結

Java IO流庫能滿足我們的許多基本要求:可以通過控制檯、文件、內存塊甚至因特網(參見第15章)進行讀寫。可以創建新的輸入和輸出對象類型(通過從InputStreamOutputStream繼承)。向一個本來預期為收到字符串的方法傳遞一個對象時,由於Java已限制了“自動類型轉換”,所以會自動調用toString()方法。而我們可以重新定義這個toString(),擴展一個數據流能接納的對象種類。

在IO數據流庫的聯機文檔和設計過程中,仍有些問題沒有解決。比如當我們打開一個文件以便輸出時,完全可以指定一旦有人試圖覆蓋該文件就“拋”出一個異常——有的編程系統允許我們自行指定想打開一個輸出文件,但唯一的前提是它尚不存在。但在Java中,似乎必須用一個File對象來判斷某個文件是否存在,因為假如將其作為FileOutputStream或者FileWriter打開,那麼肯定會被覆蓋。若同時指定文件和目錄路徑,File類設計上的一個缺陷就會暴露出來,因為它會說“不要試圖在單個類裡做太多的事情”!

IO流庫易使我們混淆一些概念。它確實能做許多事情,而且也可以移植。但假如假如事先沒有吃透裝飾器方案的概念,那麼所有的設計都多少帶有一點盲目性質。所以不管學它還是教它,都要特別花一些功夫才行。而且它並不完整:沒有提供對輸出格式化的支持,而其他幾乎所有語言的IO包都提供了這方面的支持(這一點沒有在Java 1.1裡得以糾正,它完全錯失了改變庫設計模式的機會,反而增添了更特殊的一些情況,使複雜程度進一步提高)。Java 1.1轉到那些尚未替換的IO庫,而不是增加新庫。而且庫的設計人員似乎沒有很好地指出哪些特性是不贊成的,哪些是首選的,造成庫設計中經常都會出現一些令人惱火的反對消息。

然而,一旦掌握了裝飾器方案,並開始在一些較為靈活的環境使用庫,就會認識到這種設計的好處。到那個時候,為此多付出的代碼行應該不至於使你覺得太生氣。

10.11 練習

(1) 打開一個文本文件,每次讀取一行內容。將每行作為一個String讀入,並將那個String對象置入一個Vector裡。按相反的順序打印出Vector中的所有行。

(2) 修改練習1,使讀取那個文件的名字作為一個命令行參數提供。

(3) 修改練習2,又打開一個文本文件,以便將文字寫入其中。將Vector中的行隨同行號一起寫入文件。

(4) 修改練習2,強迫Vector中的所有行都變成大寫形式,將結果發給System.out

(5) 修改練習2,在文件中查找指定的單詞。打印出包含了欲找單詞的所有文本行。

(6) 在Blips.java中複製文件,將其重命名為BlipCheck.java。然後將類Blip2重命名為BlipCheck(在進程中將其標記為public)。刪除文件中的//!記號,並執行程序。接下來,將BlipCheck的默認構造器變成註釋信息。運行它,並解釋為什麼仍然能夠工作。

(7) 在Blip3.java中,將接在"You must do this:"字樣後的兩行變成註釋,然後運行程序。解釋得到的結果為什麼會與執行了那兩行代碼不同。

(8) 轉換SortedWordCount.java程序,以便使用Java 1.1 IO流。

(9) 根據本章正文的說明修改程序CADState.java

(10) 在第7章(中間部分)找到GreenhouseControls.java示例,它應該由三個文件構成。在GreenhouseControls.java中,Restart()內部類有一個硬編碼的事件集。請修改這個程序,使其能從一個文本文件裡動態讀取事件以及它們的相關時間。