# 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`,同時將一個`String`或`File`對象作為文件名使用。為提高速度,最好先對文件進行緩沖處理,從而獲得用于一個`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`是獨立的,盡管它的數據格式兼容于`DataInputStream`和`DataOutputStream`。
(5) 保存與恢復數據
`PrintStream`能格式化數據,使其能按我們的習慣閱讀。但為了輸出數據,以便由另一個數據流恢復,則需用一個`DataOutputStream`寫入數據,并用一個`DataInputStream`恢復(獲取)數據。當然,這些數據流可以是任何東西,但這里采用的是一個文件,并進行了緩沖處理,以加快讀寫速度。
注意字符串是用`writeBytes()`寫入的,而非`writeChars()`。若使用后者,寫入的就是16位Unicode字符。由于`DataInputStream`中沒有補充的`readChars`方法,所以不得不用`readChar()`每次取出一個字符。所以對ASCII來說,更方便的做法是將字符作為字節寫入,在后面跟隨一個新行;然后再用`readLine()`將字符當作普通的ASCII行讀回。
`writeDouble()`將`double`數字保存到數據流中,并用補充的`readDouble()`恢復它。但為了保證任何讀方法能夠正常工作,必須知道數據項在流中的準確位置,因為既有可能將保存的`double`數據作為一個簡單的字節序列讀入,也有可能作為`char`或其他格式讀入。所以必須要么為文件中的數據采用固定的格式,要么將額外的信息保存到文件中,以便正確判斷數據的存放位置。
(6) 讀寫隨機訪問文件
正如早先指出的那樣,`RandomAccessFile`與IO層次結構的剩余部分幾乎是完全隔離的,盡管它也實現了DataInput和`DataOutput`接口。所以不可將其與`InputStream`及`OutputStream`子類的任何部分關聯起來。盡管也許能將一個`ByteArrayInputStream`當作一個隨機訪問元素對待,但只能用`RandomAccessFile`打開一個文件。必須假定`RandomAccessFile`已得到了正確的緩沖,因為我們不能自行選擇。
可以自行選擇的是第二個構造器參數:可決定以“只讀”(`r`)方式或“讀寫”(`rw`)方式打開一個`RandomAccessFile`文件。
使用`RandomAccessFile`的時候,類似于組合使用`DataInputStream`和`DataOutputStream`(因為它實現了等同的接口)。除此以外,還可看到程序中使用了`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.in`,`System.out`以及`System.err`。貫這一整本書,大家都會接觸到如何用`System.out`進行標準輸出,它已預封裝成一個`PrintStream`對象。
`System.err`同樣是一個`PrintStream`,但`System.in`是一個原始的`InputStream`,未進行任何封裝處理。這意味著盡管能直接使用`System.out`和`System.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章用一個示例說明。
- 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 推薦讀物