級別: 初級
Merlin Hughes (merlin@merlin.org), 密碼專家, Baltimore Technologies
2002 年 9 月 25 日
通常,Java I/O 框架用途極其廣泛。同一個框架支持文件存取、網絡訪問、字符轉換、壓縮和加密等等。不過,有時它不是十分靈活。例如,壓縮流允許您將數據寫成壓縮格式,但它們不能讓您讀取壓縮格式的數據。同樣地,某些第三方模塊被構建成寫出數據,而沒有考慮應用程序需要讀取數據的情形。本文是兩部分系列文章的第一部分,Java 密碼專家和作家 Merlin Hughes 介紹了使應用程序從僅支持將數據寫至輸出流的源中有效讀取數據的框架。
自早期基于瀏覽器的 applet 和簡單應用程序以來,Java 平臺已有了巨大的發展。現在,我們有多個平臺和概要及許多新的 API,并且還在制作的差不多有數百種之多。盡管 Java 語言的復雜程度在不斷增加,但它對于日常的編程任務而言仍是一個出色的工具。雖然有時您會陷入那些日復一日的編程問題中,但偶爾您也能夠回過頭去,發現一個很棒的解決方案來處理您以前曾多次遇到過的問題。
就在前幾天,我想要壓縮一些通過網絡連接讀取的數據(我以壓縮格式將 TCP 數據中繼到一個 UDP 套接字)。記得 Java 平臺自版本 1.1 開始就支持壓縮,所以我直接求助于 java.util.zip 包,希望能找到一個適合于我的解決方案。然而,我發現一個問題:構造的類都適用于常規情況,即在讀取時對數據解壓縮而在寫入時壓縮它們,沒有其它變通方法。雖然繞過 I/O 類是可能的,但我希望構建一個基于流的解決方案,而不想偷懶直接使用壓縮程序。
不久以前,我在另一種情況下也遇到過完全相同的問題。我有一個 base-64 轉碼庫,與使用壓縮包一樣,它支持對從流中讀取的數據進行譯碼,并對寫入流中的數據進行編碼。然而,我需要的是一個在我從流中讀取數據的同時可以進行編碼的庫。
在我著手解決該問題時,我認識到我在另一種情況下也遇到過該問題:當序列化 XML 文檔時,通常會循環遍歷整個文檔,將節點寫入流中。然而,我遇到的情況是需要讀取序列化格式的文檔,以便將子集重新解析成一個新文檔。
回過頭想一下,我意識到這些孤立事件表示了一個共性的問題:如果有一個遞增地將數據寫入輸出流的數據源,那么我需要一個輸入流使我能夠讀取這些數據,每當需要更多數據時,都能透明地訪問數據源。
在本文中,我們將研究對這一問題的三種可能的解決方案,同時決定一個實現最佳解決方案的新框架。然后,我們將針對上面列出的每個問題,檢驗該框架。我們將扼要地談及性能方面的問題,而把對此的大量討論留到下一篇文章中。
I/O 流基礎知識
首先,讓我們簡單回顧一下 Java 平臺的基本流類,如圖 1 所示。 OutputStream 表示對其寫入數據的流。通常,該流將直接連接至諸如文件或網絡連接之類的設備,或連接至另一個輸出流(在這種情況下,它稱為 過濾器(filter))。通常,輸出流過濾器在轉換了寫入其中的數據之后,才將轉換后產生的數據寫入相連的流中。 InputStream 表示可以從中讀取數據的流。同樣,該流也直接連接至設備或其它流。輸入流過濾器從相連的流中讀取數據,轉換該數據,然后允許從中讀取轉換后的數據。
圖 1. I/O 流基礎知識
就我最初的問題看, GZIPOutputStream 類是一個輸出流過濾器,它壓縮寫入其中的數據,然后將該壓縮數據寫入相連的流。我需要的輸入流過濾器應該能從流中讀取數據,壓縮數據,然后讓我讀取結果。
Java 平臺,版本 1.4 已引入了一個新的 I/O 框架 java.nio 。不過,該框架在很大程度上與提供對操作系統 I/O 資源的有效訪問有關;而且,雖然它確實為一些傳統的 java.io 類提供了類似功能,并可以表示同時支持輸入和輸出的雙重用途的資源,但它并不能完全替代標準流類,并且不能直接處理我需要解決的問題。
蠻力解決方案
在著手尋找解決我問題的工程方案前,我根據標準 Java API 類的精致和有效性,研究了基于這些類的解決方案。
該問題的蠻力解決方案就是簡單地從輸入源中讀取所有數據,然后通過轉換程序(即,壓縮流、編碼流或 XML 序列化器)將它們推進內存緩沖區中。然后,我可以從該內存緩沖區中打開要讀取的流,這樣我就解決了問題。
首先,我需要一個通用的 I/O 方法。清單 1 中的方法利用一個小緩沖區將 InputStream 中的所有數據復制到 OutputStream 。當到達輸入的結尾( read() 函數的返回值小于零)時,該方法就返回,但不關閉這兩個流。
清單 1. 通用的 I/O 方法
public static void io (InputStream in, OutputStream out)
throws IOException {
byte[] buffer = new byte[8192];
int amount;
while ((amount = in.read (buffer)) >= 0)
out.write (buffer, 0, amount);
}
|
清單 2 顯示蠻力解決方案如何使我讀取壓縮格式的輸入流。我打開寫入內存緩沖區的 GZIPOutputStream (使用 ByteArrayOutputStream )。接著,將輸入流復制到壓縮流中,這樣將壓縮數據填入內存緩沖區中。然后,我返回 ByteArrayInputStream ,它讓我從輸入流中讀取,如圖 2 所示。
圖 2. 蠻力解決方案
清單 2. 蠻力解決方案
public static InputStream bruteForceCompress (InputStream in)
throws IOException {
ByteArrayOutputStream sink = new ByteArrayOutputStream ():
OutputStream out = new GZIPOutputStream (sink);
io (in, out);
out.close ();
byte[] buffer = sink.toByteArray ();
return new ByteArrayInputStream (buffer);
}
|
這個解決方案有一個明顯的缺點,它將整個壓縮文檔都存儲在內存中。如果文檔很大,那么這種方法將不必要地浪費系統資源。使用流的主要特性之一是它們允許您操作比所用系統內存要大的數據:您可以在讀取數據時處理它們,或在寫入數據時生成數據,而無需始終將所有數據保存在內存中。
從效率上,讓我們對在緩沖區之間復制數據進行更深入研究。
通過 io() 方法,將數據從輸入源讀入至一個緩沖區中。然后,將數據從緩沖區寫入 ByteArrayOutputStream 中的緩沖區(通過我忽略的壓縮過程)。然而, ByteArrayOutputStream 類對擴展的內部緩沖區進行操作;每當緩沖區變滿時,就會分配一個大小是原來兩倍的新緩沖區,接著將現有的數據復制到該緩沖區中。平均下來,這一過程每個字節復制兩次。(算術計算很簡單:當進入 ByteArrayOutputStream 時,對數據平均復制兩次;所有數據至少復制一次;有一半數據至少復制兩次;四分之一的數據至少復制三次,依次類推。)然后,將數據從該緩沖區復制到 ByteArrayInputStream 的一個新緩沖區中。現在,應用程序可以讀取數據了。總之,這個解決方案將通過四個緩沖區寫數據。這對于估計其它技術的效率是一個有用的基準。
管道式流解決方案
管道式流 PipedOutputStream 和 PipedInputStream 在 Java 虛擬機的線程之間提供了基于流的連接。一個線程將數據寫入 PipedOutputStream 中的同時,另一個線程可以從相關聯的 PipedInputStream 中讀取該數據。
就這樣,這些類提供了一個針對我問題的解決方案。清單 3 顯示了使用一個線程通過 GZIPOutputStream 將數據從輸入流復制到 PipedOutputStream 的代碼。然后,相關聯的 PipedInputStream 將提供對來自另一個線程的壓縮數據的讀取權,如圖 3 所示:
圖 3. 管道式流解決方案
清單 3. 管道式流解決方案
private static InputStream pipedCompress (final InputStream in)
throws IOException {
PipedInputStream source = new PipedInputStream ();
final OutputStream out =
new GZIPOutputStream (new PipedOutputStream (source));
new Thread () {
public void run () {
try {
Streams.io (in, out);
out.close ();
} catch (IOException ex) {
ex.printStackTrace ();
}
}
}.start ();
return source;
}
|
理論上,這可能是個好技術:通過使用線程(一個執行壓縮,另一個處理產生的數據),應用程序可以從硬件 SMP(對稱多處理)或 SMT(對稱多線程)中受益。另外,這一解決方案僅涉及兩個緩沖區寫操作:I/O 循環將數據從輸入流讀入緩沖區,然后通過壓縮流寫入 PipedOutputStream 。接著,輸出流將數據存儲在內部緩沖區中,與 PipedInputStream 共享緩沖區以供應用程序讀取。而且,因為數據通過固定緩沖區流動,所以從不需要將它們完全讀入內存中。事實上,在任何給定時刻,緩沖區都只存儲小部分的工作集。
不過,實際上,它的性能很糟糕。管道式流需要利用同步,從而引起兩個線程之間激烈爭奪同步。它們的內部緩沖區太小,無法有效地處理大量數據或隱藏鎖爭用。其次,持久共享緩沖區會阻礙許多簡單的高速緩存策略共享 SMP 機器上的工作負載。最后,線程的使用使得異常處理極其困難:沒有辦法將可能出現的任何 IOException 下推到管道中以便閱讀器處理。總之,這一解決方案太難處理,根本不實際。
 |
同步問題
本文中提供的代碼都不能同步;也就是說,兩個線程并發地訪問其中一個類的共享實例是不安全的。
因為如 NIO 框架和 Collections API 之類的庫已經公認是實用的,所以使得同步成為應用程序中的一種負擔。如果應用程序希望對一個對象進行并發訪問,應用程序必須采取必要的步驟來同步訪問。
雖然最近的 JVM 已在其線程安全性機制的性能上有了很大的改進,但同步仍是一個開銷很大的操作。在 I/O 的情況下,對單個流的并發訪問幾乎必定是一個錯誤;結果數據流的次序是不確定的,這不是理想的情形。正因為這樣,要同步這些類會強加一些不必要的費用,又沒有確實的收益。
我們將在這一系列文章的第 2 部分更詳細討論多線程的考慮事項;目前,只要注意:對我所提供的對流的并發訪問將導致不確定錯誤。
|
|
工程解決方案
現在,我們將研究另一種解決該問題的工程方案。這種解決方案提供了一個特地為解決這類問題而設計的框架,該框架提供了對數據的 InputStream 訪問,這些數據是從遞增地向 OutputStream 寫入數據的源中產生的。遞增地寫入數據這一事實很重要。如果源在單個原子操作中將所有數據都寫入 OutputStream ,而且如果不使用線程,則我們基本上又回到了蠻力技術的老路上。不過,如果可以訪問源以遞增地寫入其數據,則我們就實現了在蠻力和管道式流解決方案之間的良好平衡。該解決方案不僅提供了在任何時候只在內存中保存少量數據的管道式優點,同時也提供了避免線程的蠻力技術的優點。
圖 4 演示了完整的解決方案。我們將在本文的剩余部分研究 該解決方案的源代碼。
圖 4. 工程解決方案
輸出引擎
清單 4 提供了一個描述數據源的接口 OutputEngine 。正如我所說的,這些源遞增地將數據寫入輸出流:
清單 4. 輸出引擎
package org.merlin.io;
import java.io.*;
/**
* An incremental data source that writes data to an OutputStream.
*
* @author Copyright (c) 2002 Merlin Hughes <merlin@merlin.org>
*
* This program is free software; you can redistribute
* it and/or modify it under the terms of the GNU
* General Public License as published by the Free
* Software Foundation; either version 2
* of the License, or (at your option) any later version.
*/
public interface OutputEngine {
public void initialize (OutputStream out) throws IOException;
public void execute () throws IOException;
public void finish () throws IOException;
}
|
initialize() 方法向該引擎提供一個流,應該向這個流寫入數據。然后,重復調用 execute() 方法將數據寫入該流中。當數據寫完時,引擎會關閉該流。最后,當引擎應該關閉時,將調用 finish() 。這會發生在引擎關閉其輸出流的前后。
I/O 流引擎
輸出引擎解決了讓我費力處理的問題,它是一個通過輸出流過濾器將數據從輸入流復制到目標輸出流的引擎。這滿足了遞增性的特性,因為它可以一次讀寫單個緩沖區。
清單 5 到 10 中的代碼實現了這樣的一個引擎。通過輸入流和輸入流工廠來構造它。清單 11 是一個生成過濾后的輸出流的工廠;例如,它會返回包裝了目標輸出流的 GZIPOutputStream 。
清單 5. I/O 流引擎
package org.merlin.io;
import java.io.*;
/**
* An output engine that copies data from an InputStream through
* a FilterOutputStream to the target OutputStream.
*
* @author Copyright (c) 2002 Merlin Hughes <merlin@merlin.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*/
public class IOStreamEngine implements OutputEngine {
private static final int DEFAULT_BUFFER_SIZE = 8192;
private InputStream in;
private OutputStreamFactory factory;
private byte[] buffer;
private OutputStream out;
|
該類的構造器只初始化各種變量和將用于傳輸數據的緩沖區。
清單 6. 構造器
public IOStreamEngine (InputStream in, OutputStreamFactory factory) {
this (in, factory, DEFAULT_BUFFER_SIZE);
}
public IOStreamEngine
(InputStream in, OutputStreamFactory factory, int bufferSize) {
this.in = in;
this.factory = factory;
buffer = new byte[bufferSize];
}
|
在 initialize() 方法中,該引擎調用其工廠來封裝與其一起提供的 OutputStream 。該工廠通常將一個過濾器連接至 OutputStream 。
清單 7. initialize() 方法
public void initialize (OutputStream out) throws IOException {
if (this.out != null) {
throw new IOException ("Already initialised");
} else {
this.out = factory.getOutputStream (out);
}
}
|
在 execute() 方法中,引擎從 InputStream 中讀取一個緩沖區的數據,然后將它們寫入已封裝的 OutputStream ;或者,如果輸入結束,它會關閉 OutputStream 。
清單 8. execute() 方法
public void execute () throws IOException {
if (out == null) {
throw new IOException ("Not yet initialised");
} else {
int amount = in.read (buffer);
if (amount < 0) {
out.close ();
} else {
out.write (buffer, 0, amount);
}
}
}
|
最后,當關閉引擎時,它就關閉其 InputStream 。
清單 9. 關閉 InputStream
public void finish () throws IOException {
in.close ();
}
|
內部 OutputStreamFactory 接口(下面清單 10 中所示)描述可以返回過濾后的 OutputStream 的類。
清單 10. 內部輸出流工廠接口
public static interface OutputStreamFactory {
public OutputStream getOutputStream (OutputStream out)
throws IOException;
}
}
|
清單 11 顯示將提供的流封裝到 GZIPOutputStream 中的一個示例工廠:
清單 11. GZIP 輸出流工廠
public class GZIPOutputStreamFactory
implements IOStreamEngine.OutputStreamFactory {
public OutputStream getOutputStream (OutputStream out)
throws IOException {
return new GZIPOutputStream (out);
}
}
|
該 I/O 流引擎及其輸出流工廠框架通常足以支持大多數的輸出流過濾需要。
輸出引擎輸入流
最后,我們還需要一小段代碼來完成這個解決方案。清單 12 到 16 中的代碼提供了讀取由輸出引擎所寫數據的輸入流。事實上,這段代碼有兩個部分:主類是一個從內部緩沖區讀取數據的輸入流。與此緊密耦合的是一個輸出流(如清單 17 所示),它把輸出引擎所寫的數據填充到內部讀緩沖區。
主輸入流類將用其內部輸出流來初始化輸出引擎。然后,每當它的緩沖區為空時,它會自動執行該引擎來接收更多數據。輸出引擎將數據寫入其輸出流中,這將重新填充輸入流的內部緩沖區,以允許需要內部緩沖區數據的應用程序高效地讀取數據。
清單 12. 輸出引擎輸入流
package org.merlin.io;
import java.io.*;
/**
* An input stream that reads data from an OutputEngine.
*
* @author Copyright (c) 2002 Merlin Hughes <merlin@merlin.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*/
public class OutputEngineInputStream extends InputStream {
private static final int DEFAULT_INITIAL_BUFFER_SIZE = 8192;
private OutputEngine engine;
private byte[] buffer;
private int index, limit, capacity;
private boolean closed, eof;
|
該輸入流的構造器獲取一個輸出引擎以從中讀取數據和一個可選的緩沖區大小。該流首先初始化其本身,然后初始化輸出引擎。
清單 13. 構造器
public OutputEngineInputStream (OutputEngine engine) throws IOException {
this (engine, DEFAULT_INITIAL_BUFFER_SIZE);
}
public OutputEngineInputStream (OutputEngine engine, int initialBufferSize)
throws IOException {
this.engine = engine;
capacity = initialBufferSize;
buffer = new byte[capacity];
engine.initialize (new OutputStreamImpl ());
}
|
代碼的主要讀部分是一個相對簡單的基于字節數組的輸入流,與 ByteArrayInputStream 類非常相似。然而,每當需要數據而該流為空時,它都會調用輸出引擎的 execute() 方法來重新填寫讀緩沖區。然后,將這些新數據返回給調用程序。因而,這個類將對輸出引擎所寫的數據反復讀取,直到它讀完為止,此時將設置 eof 標志并且該流將返回已到達文件末尾的信息。
清單 14. 讀取數據
private byte[] one = new byte[1];
public int read () throws IOException {
int amount = read (one, 0, 1);
return (amount < 0) ? -1 : one[0] & 0xff;
}
public int read (byte data[], int offset, int length)
throws IOException {
if (data == null) {
throw new NullPointerException ();
} else if
((offset < 0) || (length < 0) || (offset + length > data.length)) {
throw new IndexOutOfBoundsException ();
} else if (closed) {
throw new IOException ("Stream closed");
} else {
while (index >= limit) {
if (eof)
return -1;
engine.execute ();
}
if (limit - index < length)
length = limit - index;
System.arraycopy (buffer, index, data, offset, length);
index += length;
return length;
}
}
public long skip (long amount) throws IOException {
if (closed) {
throw new IOException ("Stream closed");
} else if (amount <= 0) {
return 0;
} else {
while (index >= limit) {
if (eof)
return 0;
engine.execute ();
}
if (limit - index < amount)
amount = limit - index;
index += (int) amount;
return amount;
}
}
public int available () throws IOException {
if (closed) {
throw new IOException ("Stream closed");
} else {
return limit - index;
}
}
|
當操作數據的應用程序關閉該流時,它調用輸出引擎的 finish() 方法,以便可以釋放其正在使用的任何資源。
清單 15. 釋放資源
public void close () throws IOException {
if (!closed) {
closed = true;
engine.finish ();
}
}
|
當輸出引擎將數據寫入其輸出流時,調用 writeImpl() 方法。它將這些數據復制到讀緩沖區,并更新讀限制索引;這將使新數據可自動地用于讀方法。
在單次循環中,如果輸出引擎寫入的數據比緩沖區中可以保存的數據多,則緩沖區的容量會翻倍。然而,這不能頻繁發生;緩沖區應該快速擴展到足夠的大小,以便進行狀態穩定的操作。
清單 16. writeImpl() 方法
private void writeImpl (byte[] data, int offset, int length) {
if (index >= limit)
index = limit = 0;
if (limit + length > capacity) {
capacity = capacity * 2 + length;
byte[] tmp = new byte[capacity];
System.arraycopy (buffer, index, tmp, 0, limit - index);
buffer = tmp;
limit -= index;
index = 0;
}
System.arraycopy (data, offset, buffer, limit, length);
limit += length;
}
|
下面清單 17 中顯示的內部輸出流實現表示了一個流將數據寫入內部輸出流緩沖區。該代碼驗證參數都是可接受的,并且如果是這樣的話,它調用 writeImpl() 方法。
清單 17. 內部輸出流實現
private class OutputStreamImpl extends OutputStream {
public void write (int datum) throws IOException {
one[0] = (byte) datum;
write (one, 0, 1);
}
public void write (byte[] data, int offset, int length)
throws IOException {
if (data == null) {
throw new NullPointerException ();
} else if
((offset < 0) || (length < 0) || (offset + length > data.length)) {
throw new IndexOutOfBoundsException ();
} else if (eof) {
throw new IOException ("Stream closed");
} else {
writeImpl (data, offset, length);
}
}
|
最后,當輸出引擎關閉其輸出流,表明它已寫入了所有的數據時,該輸出流設置輸入流的 eof 標志,表明已經讀取了所有的數據。
清單 18. 設置輸入流的 eof 標志
public void close () {
eof = true;
}
}
}
|
敏感的讀者可能注意到我應該將 writeImpl() 方法的主體直接放在輸出流實現中:內部類有權訪問所有包含類的私有成員。然而,對這些字段的內部類訪問比由包含類的直接方法的訪問在效率方面稍許差一些。所以,考慮到效率以及為了使類之間的相關性最小化,我使用額外的助手方法。
應用工程解決方案:在讀取期間壓縮數據
清單 19 演示了這個類框架的使用來解決我最初的問題:在我讀取數據時壓縮它們。該解決方案歸結為創建一個與輸入流相關聯的 IOStreamEngine 和一個 GZIPOutputStreamFactory ,然后將 OutputEngineInputStream 與這個 GZIPOutputStreamFactory 相連。自動執行流的初始化和連接,然后可以直接從結果流中讀取壓縮數據。當處理完成且關閉流時,輸出引擎自動關閉,并且它關閉初始輸入流。
清單 19. 應用工程解決方案
private static InputStream engineCompress (InputStream in)
throws IOException {
return new OutputEngineInputStream
(new IOStreamEngine (in, new GZIPOutputStreamFactory ()));
}
|
雖然為解決這類問題而設計的解決方案應該產生十分清晰的代碼,這一點沒有什么可驚奇的,但是通常要充分留意以下教訓:無論問題大小,應用良好的設計技術都幾乎肯定會產生更為清晰、更便于維護的代碼。
測試性能
從效率看, IOStreamEngine 將數據讀入其內部緩沖區,然后通過壓縮過濾器將它們寫入 OutputStreamImpl 。這將數據直接寫入 OutputEngineInputStream ,以便它們可供讀取。總共只執行兩次緩沖區復制,這意味著我應該從管道式流解決方案的緩沖區復制效率和蠻力解決方案的無線程效率的結合中獲益。
要測試實際的性能,我編寫了一個簡單的測試工具(請參閱所附 資源中的 test.PerformanceTest ),它使用這三個推薦的解決方案,通過使用一個空過濾器來讀取一塊啞元數據。在運行 Java 2 SDK,版本 1.4.0 的 800 MHz Linux 機器上,達到了下列性能:
管道式流解決方案
15KB:23ms;15MB:22100ms
蠻力解決方案
15KB:0.35ms;15MB:745ms
工程解決方案
15KB:0.16ms;15MB:73ms
該問題的工程解決方案很明顯比基于標準 Java API 的另兩個方法都更有效。
順便提一下,考慮到如果輸出引擎能夠遵守這樣的約定:在將數據寫入其輸出流后,它不修改從中寫入數據的數組而返回,那么我能提供一個只使用一次緩沖區復制操作的解決方案。可是,輸出引擎很少會遵守這種約定。如果需要,輸出引擎只要通過實現適當的標記程序接口,就能宣稱它支持這種方式的操作。
應用工程解決方案:讀取編碼的字符數據
任何可以用“提供對將數據反復寫入 OutputStream 的實體的讀訪問權”表述的問題,都可以用這一框架解決。在這一節和下一節中,我們將研究這樣的問題示例及其有效的解決方案。
首先,考慮要讀取 UTF-8 編碼格式的字符流的情況: InputStreamReader 類讓您將以二進制編碼的字符數據作為一系列 Unicode 字符讀取;它表示了從字節輸入流到字符輸入流的關口。 OutputStreamWriter 類讓您將一系列二進制編碼格式的 Unicode 字符寫入輸出流;它表示從字符輸出流到字節輸入流的關口。 String 類的 getBytes() 方法將字符串轉換成經編碼的字節數組。然而,這些類中沒有一個能直接讓您讀取 UTF-8 編碼格式的字符流。
清單 20 到 24 中的代碼演示了以與 IOStreamEngine 類極其相似的方式使用 OutputEngine 框架的一種解決方案。我們并不是從輸入流讀取和通過輸出流過濾器進行寫操作,而是從字符流讀取,并通過所選的字符進行編碼的 OutputStreamWriter 進行寫操作。
清單 20. 讀取編碼的字符數據
package org.merlin.io;
import java.io.*;
/**
* An output engine that copies data from a Reader through
* a OutputStreamWriter to the target OutputStream.
*
* @author Copyright (c) 2002 Merlin Hughes <merlin@merlin.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*/
public class ReaderWriterEngine implements OutputEngine {
private static final int DEFAULT_BUFFER_SIZE = 8192;
private Reader reader;
private String encoding;
private char[] buffer;
private Writer writer;
|
該類的構造器接受要從中讀取的字符流、要使用的編碼以及可選的緩沖區大小。
清單 21. 構造器
public ReaderWriterEngine (Reader in, String encoding) {
this (in, encoding, DEFAULT_BUFFER_SIZE);
}
public ReaderWriterEngine
(Reader reader, String encoding, int bufferSize) {
this.reader = reader;
this.encoding = encoding;
buffer = new char[bufferSize];
}
|
當該引擎初始化時,它將以所選編碼格式寫字符的 OutputStreamWriter 連接至提供的輸出流。
清單 22. 初始化輸出流寫程序
public void initialize (OutputStream out) throws IOException {
if (writer != null) {
throw new IOException ("Already initialised");
} else {
writer = new OutputStreamWriter (out, encoding);
}
}
|
當執行該引擎時,它從輸入字符流中讀取數據,然后將它們寫入 OutputStreamWriter ,接著 OutputStreamWriter 將它們以所選的編碼格式傳遞給相連的輸出流。至此,該框架使數據可供讀取。
清單 23. 讀取數據
public void execute () throws IOException {
if (writer == null) {
throw new IOException ("Not yet initialised");
} else {
int amount = reader.read (buffer);
if (amount < 0) {
writer.close ();
} else {
writer.write (buffer, 0, amount);
}
}
}
|
當引擎執行完時,它關閉其輸入。
清單 24. 關閉輸入
public void finish () throws IOException {
reader.close ();
}
}
|
在這種與壓縮不同的情況中,Java I/O 包不提供對 OutputStreamWriter 之下的字符編碼類的低級別訪問。因此,這是在 Java 平臺 1.4 之前的發行版上讀取編碼格式的字符流的唯一有效解決方案。從版本 1.4 開始, java.nio.charset 包確實提供了與流無關的字符編碼和譯碼能力。然而,這個包不能滿足我們對基于輸入流的解決方案的要求。
應用工程解決方案:讀取序列化的 DOM 文檔
最后,讓我們研究該框架的最后一種用法。清單 25 到 29 中的代碼提供了一個用來讀取序列化格式的 DOM 文檔或文檔子集的解決方案。這一代碼的潛在用途可能是對部分 DOM 文檔執行確認性重新解析。
清單 25. 讀取序列化的 DOM 文檔
package org.merlin.io;
import java.io.*;
import java.util.*;
import org.w3c.dom.*;
import org.w3c.dom.traversal.*;
/**
* An output engine that serializes a DOM tree using a specified
* character encoding to the target OutputStream.
*
* @author Copyright (c) 2002 Merlin Hughes <merlin@merlin.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*/
public class DOMSerializerEngine implements OutputEngine {
private NodeIterator iterator;
private String encoding;
private OutputStreamWriter writer;
|
構造器獲取要在上面進行循環的 DOM 節點,或預先構造的節點迭代器(這是 DOM 2 的一部分),以及一個用于序列化格式的編碼。
清單 26. 構造器
public DOMSerializerEngine (Node root) {
this (root, "UTF-8");
}
public DOMSerializerEngine (Node root, String encoding) {
this (getIterator (root), encoding);
}
private static NodeIterator getIterator (Node node) {
DocumentTraversal dt= (DocumentTraversal)
(node.getNodeType () ==
Node.DOCUMENT_NODE) ? node : node.getOwnerDocument ();
return dt.createNodeIterator (node, NodeFilter.SHOW_ALL, null, false);
}
public DOMSerializerEngine (NodeIterator iterator, String encoding) {
this.iterator = iterator;
this.encoding = encoding;
}
|
初始化期間,該引擎將適當的 OutputStreamWriter 連接至目標輸出流。
清單 27. initialize() 方法
public void initialize (OutputStream out) throws IOException {
if (writer != null) {
throw new IOException ("Already initialised");
} else {
writer = new OutputStreamWriter (out, encoding);
}
}
|
在執行階段,該引擎從節點迭代器中獲得下一個節點,然后將其序列化至 OutputStreamWriter 。當獲取了所有節點后,引擎關閉它的流。
清單 28. execute() 方法
public void execute () throws IOException {
if (writer == null) {
throw new IOException ("Not yet initialised");
} else {
Node node = iterator.nextNode ();
closeElements (node);
if (node == null) {
writer.close ();
} else {
writeNode (node);
writer.flush ();
}
}
}
|
當該引擎關閉時,沒有要釋放的資源。
清單 29. 關閉
public void finish () throws IOException {
}
// private void closeElements (Node node) throws IOException ...
// private void writeNode (Node node) throws IOException ...
}
|
序列化每個節點的其它內部細節不太有趣;這一過程主要涉及根據節點的類型和 XML 1.0 規范寫出節點,所以我將在本文中省略這一部分的代碼。請參閱附帶的 源代碼,獲取完整的詳細信息。
結束語
我所提供的是一個有用的框架,它利用標準輸入流 API 讓您能有效讀取由只能寫入輸出流的系統產生的數據。它讓我們讀取經壓縮或編碼的數據及序列化文檔等。雖然可以使用標準 Java API 實現這一功能,但使用這些類的效率根本不行。應該充分注意到,這種解決方案比最簡單的蠻力解決方案更有效(即使在數據不大的情況下)。將數據寫入 ByteArrayOutputStream 以便進行后續處理的任何應用程序都可能從這一框架中受益。
字節數組流的拙劣性能和管道式流難以置信的蹩腳性能,實際上都是我下一篇文章的主題。在那篇文章中,我將研究重新實現這些類,并比這些類的原創者更加關注它們的性能。只要 API 約定稍微寬松一點,性能就可能改進一百倍了。
我討厭洗碗。不過,正如大多數我自認為是較好(雖然常常還是微不足道)的想法一樣,這些類背后的想法都是在我洗碗時冒出來的。我時常發現撇開實際代碼,回頭看看并且把問題的范圍考慮得更廣些,可能會得出一個更好的解決方案,它最終為您提供的方法可能比您找出的容易方法更好。這些解決方案常常會產生更清晰、更有效而且更可維護的代碼。
我真的擔心我們有了洗碗機的那一天。
隨著,java JDK 版本不停的升級, 針對于I/O的操作,也有所相應的升級. 舉例:增加了NIO包,可以針對整個文件的copy, paste 進行操作, 底層還是讀寫操作. 所以大批量的讀寫的時候,請注意代碼的優化:
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
public class Copy{
public static void main(String argv[]){
if(argv.length!=2){
System.out.println("usage>java Copy srcfilename destfilename");
System.exit(0);
}
try {
// Create channel on the source
FileChannel srcChannel = new FileInputStream(argv[0]).getChannel();
// Create channel on the destination
FileChannel dstChannel = new FileOutputStream(argv[1]).getChannel();
// Copy file contents from source to destination
dstChannel.transferFrom(srcChannel, 0, srcChannel.size());
// Close the channels
srcChannel.close();
dstChannel.close();
} catch (IOException e) {
|