http://book.51cto.com/art/200812/101093.htm
14.2 位流
電腦中的資料都是以0與1的方式來儲存,如果要在兩個裝置之間進行資料的存取,當然也是以0與1位的方式來進行,Java將資料於目的地及來源之間的流動抽象化為一個流(Stream),而流當中流動的則是位元據。
14.2.1 InputStream和OutputStream
電腦中實際上資料的流動是通過電路,而上面流動的則是電流,電流的電位有低位與高位,即數位0與1位。從程式的觀點來說,通常會將資料目的地(例如記憶體)與來源(例如檔案)之間的資料流動抽象化為一個流(Stream),而其中流動的則是位元據,14-1所示。
在Java SE中有兩個類用來作流的抽象表示:java.io.InputStream與java.io.OutputStream。
InputStream是所有表示位輸入資料流的類之父類,它是一個抽象類別,繼承它的子類要重新定義其中所定義的抽象方法。InputStream是從裝置來源地讀取資料的抽象表示,例如System中的標準輸入資料流in對象就是一個InputStream類型的執行個體。在Java程式開始之後,in流對象就會開啟,目的是從標準輸入裝置中讀取資料,這個裝置通常是鍵盤或是使用者定義的輸入裝置。
OutputStream是所有表示位輸出資料流的類之父類,它是一個抽象類別。子類要重新定義其中所定義的抽象方法,OutputStream是用於將資料寫入目的地的抽象表示。例如System中的標準輸出資料流對象out其類型是java.io.PrintStream,這個類是OutputStream的子類(java.io.FilterOutputStream繼承OutputStream, PrintStream再繼承FilterOutputStream)。在程式開始之後,out流對象就會開啟,可以通過out來將資料寫至目的地裝置,這個裝置通常是螢幕顯示或使用者定義的輸出裝置。
範例14.4可以讀取鍵盤輸入資料流,in對象的read()方法一次讀取一個位元組的資料,讀入的資料以int類型返回。所以在使用out對象將資料顯示出來時,就是10進位方式。
範例14.4 StreamDemo.java
package onlyfun.caterpillar;
import java.io.*;
public class StreamDemo { public static void main(String[] args) { try { System.out.print("輸入字元: "); System.out.println("輸入字元十進位表示: " + System.in.read()); } catch(IOException e) { e.printStackTrace(); } } } |
執行結果:
字元A輸入後由標準輸入資料流in讀取,A的位表示以十進位來看就是65,這是A字元的編碼(查查ASCII編碼錶就知道了)。
一般來說,很少直接實現InputStream或OutputStream上的方法,因為這些方法比較低級,通常會實現它們的子類。這些子類上所定義的方法在進行輸入/輸出時更為方便。
14.2.2 FileInputStream和FileOutputStream
java.io.FileInputStream是InputStream的子類。從開頭File名稱上就可以知道,FileInputStream與從指定的檔案中讀取資料至目的地有關。而java.io.FileOutputStream是OutputStream的子類,顧名思義,FileOutputStream主要與從來源地寫入資料至指定的檔案中有關。
當建立一個FileInputStream或FileOutputStream的執行個體時,必須指定檔案位置及檔案名稱,執行個體被建立時檔案的流就會開啟;而不使用流時,必須關閉檔案流,以釋放與流相依的系統資源,完成檔案讀/寫的動作。
FileInputStream可以使用read()方法一次讀入一個位元組,並以int類型返回,或者是使用read()方法時讀入至一個byte數組,byte數組的元素有多少個,就讀入多少個位元組。在將整個檔案讀取完成或寫入完畢的過程中,這麼一個byte數組通常被當作緩衝區,因為這麼一個byte數組通常扮演承接資料的中間角色。
範例14.5是使用FileInputStream與FileOutputStream的一個例子。程式可以複製檔案,它會先從來源檔案讀取資料至一個byte數組中,然後再將byte數組的資料寫入目的檔案。
範例14.5 FileStreamDemo.java
package onlyfun.caterpillar; import java.io.*; public class FileStreamDemo { public static void main(String[] args) { try { byte[] buffer = new byte[1024]; // 來源檔案 FileInputStream fileInputStream = new FileInputStream(new File(args[0])); // 目的檔案 FileOutputStream fileOutputStream = new FileOutputStream(new File(args[1])); // available()可取得未讀取的資料長度 System.out.println("複製檔案:" + fileInputStream.available() + "位元組"); while(true) { if(fileInputStream.available() < 1024) { // 剩餘的資料比1024位元組少 // 一位一位讀出再寫入目的檔案 int remain = -1; while((remain = fileInputStream.read()) != -1) { fileOutputStream.write(remain); } break; } else { // 從來源檔案讀取資料至緩衝區 fileInputStream.read(buffer); // 將數組資料寫入目的檔案 fileOutputStream.write(buffer); } } // 關閉流 fileInputStream.close(); fileOutputStream.close(); System.out.println("複製完成"); } catch(ArrayIndexOutOfBoundsException e) { System.out.println( "using: java FileStreamDemo src des"); e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } } } |
程式中示範了兩個read()方法,一個方法可以讀入指定長度的資料至數組,另一個方法一次可以讀入一個位元組。每次讀取之後,讀取的游標都會往前進,如果讀不到資料則返回-1,使用available()方法獲得還有多少位元組可以讀取。除了使用File來建立FileInputStream、FileOutputStream的執行個體之外,也可以直接使用字串指定路徑來建立。
// 來源檔案 FileInputStream fileInputStream = new FileInputStream(args[0]); // 目的檔案 FileOutputStream fileOutputStream = new FileOutputStream(args[1]); |
在不使用檔案流時,記得使用close()方法自行關閉流,以釋放與流相依的系統資源。一個執行的結果範例如下,它將FileDemo.java複製為FileDemo.txt:
java onlyfun.caterpillar.FileStreamDemo FileDemo.java FileDemo.txt 複製檔案:1723位元組 複製完成 |
FileOutputStream預設會以建立檔案的方式來開啟流。如果指定的檔案名稱已經存在,則原檔案會被覆蓋;如果想以附加的模式來寫入檔案,則可以在構建FileOutputStream執行個體時指定為附加模式。例如:
FileOutputStream fileOutputStream = new FileOutputStream(args[1], true); |
構建方法的第二個append參數如果設定為true,在開啟流時如果檔案不存在則會建立一個檔案,如果檔案存在就直接開啟流,並將寫入的資料附加至檔案末端。
14.2.3 BufferedInputStream和BufferedOutputStream
在介紹FileInputStream和FileOutputStream的例子中,使用了一個byte數組來作為資料讀入的緩衝區,以檔案存取為例,硬碟存取的速度遠低於記憶體中的資料存取速度。為了減少對硬碟的存取,通常從檔案中一次讀入一定長度的資料,而寫入時也是一次寫入一定長度的資料,這可以增加檔案存取的效率。
java.io.BufferedInputStream與java.io.BufferedOutputStream可以為InputStream、OutputStream類的對象增加緩衝區功能。構建BufferedInputStream執行個體時,需要給定一個InputStream類型的執行個體,實現BufferedInputStream時,實際上最後是實現InputStream執行個體。同樣地,在構建BufferedOutputStream時,也需要給定一個OutputStream執行個體,實現BufferedOutputStream時,實際上最後是實現OutputStream執行個體。
BufferedInputStream的資料成員buf是一個位元組,預設為2048位元組。當讀取資料來源時,例如檔案,BufferedInputStream會盡量將buf填滿。當使用read()方法時,實際上是先讀取buf中的資料,而不是直接對資料來源作讀取。當buf中的資料不足時,BufferedInputStream才會再實現給定的InputStream對象的read()方法,從指定的裝置中提取資料,14-2所示。
BufferedOutputStream的資料成員buf是一個位元組,預設為512位元組。當使用write()方法寫入資料時,實際上會先將資料寫至buf中,當buf已滿時才會實現給定的OutputStream對象的write()方法,將buf資料寫至目的地,而不是每次都對目的地作寫入的動作。
下面將範例14.5做個改寫,這次不用自行設定緩衝區,而使用BufferedInputStream和BufferedOutputStream讓程式看來簡單一些,也比較有效率。
範例14.6 BufferedStreamDemo.java
package onlyfun.caterpillar;
import java.io.*;
public class BufferedStreamDemo { public static void main(String[] args) { try { byte[] data = new byte[1]; File srcFile = new File(args[0]); File desFile = new File(args[1]); BufferedInputStream bufferedInputStream = new BufferedInputStream( new FileInputStream(srcFile)); BufferedOutputStream bufferedOutputStream = new BufferedOutputStream( new FileOutputStream(desFile)); System.out.println("複製檔案:" + srcFile.length() + "位元組"); while(bufferedInputStream.read(data) != -1) { bufferedOutputStream.write(data); } // 將緩衝區中的資料全部寫出 bufferedOutputStream.flush(); // 關閉流 bufferedInputStream.close(); bufferedOutputStream.close(); System.out.println("複製完成"); } catch(ArrayIndexOutOfBoundsException e) { System.out.println( "using: java UseFileStream src des"); e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } } } |
為了確保緩衝區中的資料一定被寫出至目的地,建議最後執行flush()將緩衝區中的資料全部寫出目的流中。這個範例的執行結果與範例14.5是相同的。
BufferedInputStream和BufferedOutputStream並沒有改變InputStream或 OutputStream的行為,讀入或寫出時的動作還是InputStream和OutputStream負責。BufferedInputStream和BufferedOutputStream只是在操作對應的方法之前,動態地為它們加上一些額外功能(像緩衝區功能),在這裡是以檔案存取流為例,實際上可以在其他流對象上也使用BufferedInputStream和BufferedOutputStream功能。
14.2.4 DataInputStream和DataOutputStream
java.io.DataInputStream和java.io.DataOutputStream可提供一些對Java基礎資料型別 (Elementary Data Type)寫入的方法,像讀寫int、double和boolean等的方法。由於Java的資料類型大小是規定好的,在寫入或讀出這些基礎資料型別 (Elementary Data Type)時,就不用擔心不同平台間資料大小不同的問題。
這裡還是以檔案存取來進行說明。有時只是要儲存一個對象的成員資料,而不是整個對象的資訊,成員資料的類型假設都是Java的基礎資料型別 (Elementary Data Type),這樣的需求不必要使用到與Object輸入、輸出相關的流對象,可以使用DataInputStream、DataOutputStream來寫入或讀出資料。
下面使用範例來介紹如何使用DataInputStream與DataOutputStream。先設計一個Member類。
範例14.7 Member.java
package onlyfun.caterpillar; public class Member { private String name; private int age; public Member() { } public Member(String name, int age) { this.name = name; this.age = age; } public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public int getAge() { return age; } } |
打算將Member類執行個體的成員資料寫入檔案中,並打算在讀入檔案資料後,將這些資料還原為Member對象。範例14.8簡單示範了如何?這個需求。
範例14.8 DataStreamDemo.java
package onlyfun.caterpillar;
import java.io.*;
public class DataStreamDemo { public static void main(String[] args) { Member[] members = {new Member("Justin", 90), new Member("momor", 95), new Member("Bush", 88)}; try { DataOutputStream dataOutputStream = new DataOutputStream( new FileOutputStream(args[0]));
for(Member member : members) { // 寫入UTF字串 dataOutputStream.writeUTF(member.getName()); // 寫入int資料 dataOutputStream.writeInt(member.getAge()); } // 讀出所有資料至目的地 dataOutputStream.flush(); // 關閉流 dataOutputStream.close();
DataInputStream dataInputStream = new DataInputStream( new FileInputStream(args[0])); // 讀出資料並還原為對象 for(int i = 0; i < members.length; i++) { // 讀出UTF字串 String name = dataInputStream.readUTF(); // 讀出int資料 int score = dataInputStream.readInt(); members[i] = new Member(name, score); } // 關閉流 dataInputStream.close(); // 顯示還原後的資料 for(Member member : members) { System.out.printf("%s/t%d%n", member.getName(), member.getAge()); } } catch(IOException e) { e.printStackTrace(); } } } |
在從檔案中讀出資料時,不用費心地自行判斷讀入字串時或讀入int類型時何時該停止,使用對應的readUTF()或readInt()方法就可以正確地讀入完整類型資料。同樣地,DataInputStream、DataOutputStream並沒有改變InputStream或OutputStream的行為,讀入或寫出時的動作還是InputStream、OutputStream負責。DataInputStream、DataOutputStream只是在實現對應的方法時,動態地為它們加上類型判斷功能,在這裡雖然是以檔案存取流為例,實際上可以在其他流對象上也使用DataInputStream、DataOutputStream功能。