# 10.7 Java 1.1的IO流
到這個時候,大家或許會陷入一種困境之中,懷疑是否存在IO流的另一種設計模式,并可能要求更大的代碼量。還有人能提出一種更古怪的設計嗎?事實上,Java 1.1對IO流庫進行了一些重大的改進。看到`Reader`和`Writer`類時,大多數人的第一個印象(就象我一樣)就是它們用來替換原來的`InputStream`和`OutputStream`類。但實情并非如此。盡管不建議使用原始數據流庫的某些功能(如使用它們,會從編譯器收到一條警告消息),但原來的數據流依然得到了保留,以便維持向后兼容,而且:
(1) 在老式層次結構里加入了新類,所以Sun公司明顯不會放棄老式數據流。
(2) 在許多情況下,我們需要與新結構中的類聯合使用老結構中的類。為達到這個目的,需要使用一些“橋”類:
`InputStreamReader`將一個`InputStream`轉換成`Reader`,`OutputStreamWriter`將一個`OutputStream`轉換成`Writer`。
所以與原來的IO流庫相比,經常都要對新IO流進行層次更多的封裝。同樣地,這也屬于裝飾器方案的一個缺點——需要為額外的靈活性付出代價。
之所以在Java 1.1里添加了`Reader`和`Writer`層次,最重要的原因便是國際化的需求。老式IO流層次結構只支持8位字節流,不能很好地控制16位Unicode字符。由于Unicode主要面向的是國際化支持(Java內含的`char`是16位的Unicode),所以添加了`Reader`和`Writer`層次,以提供對所有IO操作中的Unicode的支持。除此之外,新庫也對速度進行了優化,可比舊庫更快地運行。
與本書其他地方一樣,我會試著提供對類的一個概述,但假定你會利用聯機文檔搞定所有的細節,比如方法的詳盡列表等。
## 10.7.1 數據的發起與接收
Java 1.0的幾乎所有IO流類都有對應的Java 1.1類,用于提供內建的Unicode管理。似乎最容易的事情就是“全部使用新類,再也不要用舊的”,但實際情況并沒有這么簡單。有些時候,由于受到庫設計的一些限制,我們不得不使用Java 1.0的IO流類。特別要指出的是,在舊流庫的基礎上新加了`java.util.zip`庫,它們依賴舊的流組件。所以最明智的做法是“嘗試性”地使用`Reader`和`Writer`類。若代碼不能通過編譯,便知道必須換回老式庫。
下面這張表格分舊庫與新庫分別總結了信息發起與接收之間的對應關系。
```
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中,數據流通過`FilterInputStream`和`FilterOutputStream`的“裝飾器”(Decorator)子類適應特定的需求。Java 1.1的IO流沿用了這一思想,但沒有繼續采用所有裝飾器都從相同`filter`(過濾器)基類中派生這一做法。若通過觀察類的層次結構來理解它,這可能令人出現少許的困惑。
在下面這張表格中,對應關系比上一張表要粗糙一些。之所以會出現這個差別,是由類的組織造成的:盡管`BufferedOutputStream`是`FilterOutputStream`的一個子類,但是`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`,所以為了用一種可轉移的格式保存和獲取數據,必須沿用`InputStream`和`OutputStream`層次結構。
## 10.7.4 一個例子
為體驗新類的效果,下面讓我們看看如何修改`IOStreamDemo.java`示例的相應區域,以便使用`Reader`和`Writer`類:
```
//: 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節中,我們被強迫使用所有的老式數據流,因為`DataOutputStream`和`DataInputStream`要求用到它們,而且沒有可供替換的東西。然而,編譯期間不會產生任何“反對”信息。若不贊成一種數據流,通常是由于它的構造器產生了一條反對消息,禁止我們使用整個類。但在`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`呢?畢竟,后者是一個嶄新和首選的替換措施呀?這真令人費解。
- Java 編程思想
- 寫在前面的話
- 引言
- 第1章 對象入門
- 1.1 抽象的進步
- 1.2 對象的接口
- 1.3 實現方案的隱藏
- 1.4 方案的重復使用
- 1.5 繼承:重新使用接口
- 1.6 多態對象的互換使用
- 1.7 對象的創建和存在時間
- 1.8 異常控制:解決錯誤
- 1.9 多線程
- 1.10 永久性
- 1.11 Java和因特網
- 1.12 分析和設計
- 1.13 Java還是C++
- 第2章 一切都是對象
- 2.1 用引用操縱對象
- 2.2 所有對象都必須創建
- 2.3 絕對不要清除對象
- 2.4 新建數據類型:類
- 2.5 方法、參數和返回值
- 2.6 構建Java程序
- 2.7 我們的第一個Java程序
- 2.8 注釋和嵌入文檔
- 2.9 編碼樣式
- 2.10 總結
- 2.11 練習
- 第3章 控制程序流程
- 3.1 使用Java運算符
- 3.2 執行控制
- 3.3 總結
- 3.4 練習
- 第4章 初始化和清除
- 4.1 用構造器自動初始化
- 4.2 方法重載
- 4.3 清除:收尾和垃圾收集
- 4.4 成員初始化
- 4.5 數組初始化
- 4.6 總結
- 4.7 練習
- 第5章 隱藏實現過程
- 5.1 包:庫單元
- 5.2 Java訪問指示符
- 5.3 接口與實現
- 5.4 類訪問
- 5.5 總結
- 5.6 練習
- 第6章 類復用
- 6.1 組合的語法
- 6.2 繼承的語法
- 6.3 組合與繼承的結合
- 6.4 到底選擇組合還是繼承
- 6.5 protected
- 6.6 累積開發
- 6.7 向上轉換
- 6.8 final關鍵字
- 6.9 初始化和類裝載
- 6.10 總結
- 6.11 練習
- 第7章 多態性
- 7.1 向上轉換
- 7.2 深入理解
- 7.3 覆蓋與重載
- 7.4 抽象類和方法
- 7.5 接口
- 7.6 內部類
- 7.7 構造器和多態性
- 7.8 通過繼承進行設計
- 7.9 總結
- 7.10 練習
- 第8章 對象的容納
- 8.1 數組
- 8.2 集合
- 8.3 枚舉器(迭代器)
- 8.4 集合的類型
- 8.5 排序
- 8.6 通用集合庫
- 8.7 新集合
- 8.8 總結
- 8.9 練習
- 第9章 異常差錯控制
- 9.1 基本異常
- 9.2 異常的捕獲
- 9.3 標準Java異常
- 9.4 創建自己的異常
- 9.5 異常的限制
- 9.6 用finally清除
- 9.7 構造器
- 9.8 異常匹配
- 9.9 總結
- 9.10 練習
- 第10章 Java IO系統
- 10.1 輸入和輸出
- 10.2 增添屬性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File類
- 10.5 IO流的典型應用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 壓縮
- 10.9 對象序列化
- 10.10 總結
- 10.11 練習
- 第11章 運行期類型識別
- 11.1 對RTTI的需要
- 11.2 RTTI語法
- 11.3 反射:運行期類信息
- 11.4 總結
- 11.5 練習
- 第12章 傳遞和返回對象
- 12.1 傳遞引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只讀類
- 12.5 總結
- 12.6 練習
- 第13章 創建窗口和程序片
- 13.1 為何要用AWT?
- 13.2 基本程序片
- 13.3 制作按鈕
- 13.4 捕獲事件
- 13.5 文本字段
- 13.6 文本區域
- 13.7 標簽
- 13.8 復選框
- 13.9 單選鈕
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 視窗化應用
- 13.16 新型AWT
- 13.17 Java 1.1用戶接口API
- 13.18 可視編程和Beans
- 13.19 Swing入門
- 13.20 總結
- 13.21 練習
- 第14章 多線程
- 14.1 反應靈敏的用戶界面
- 14.2 共享有限的資源
- 14.3 堵塞
- 14.4 優先級
- 14.5 回顧runnable
- 14.6 總結
- 14.7 練習
- 第15章 網絡編程
- 15.1 機器的標識
- 15.2 套接字
- 15.3 服務多個客戶
- 15.4 數據報
- 15.5 一個Web應用
- 15.6 Java與CGI的溝通
- 15.7 用JDBC連接數據庫
- 15.8 遠程方法
- 15.9 總結
- 15.10 練習
- 第16章 設計模式
- 16.1 模式的概念
- 16.2 觀察器模式
- 16.3 模擬垃圾回收站
- 16.4 改進設計
- 16.5 抽象的應用
- 16.6 多重分發
- 16.7 訪問器模式
- 16.8 RTTI真的有害嗎
- 16.9 總結
- 16.10 練習
- 第17章 項目
- 17.1 文字處理
- 17.2 方法查找工具
- 17.3 復雜性理論
- 17.4 總結
- 17.5 練習
- 附錄A 使用非JAVA代碼
- 附錄B 對比C++和Java
- 附錄C Java編程規則
- 附錄D 性能
- 附錄E 關于垃圾收集的一些話
- 附錄F 推薦讀物