11.3.3 裝飾流使用
除了按照流的方向可以把流劃分為輸入流和輸出流兩類,按照流讀寫數據的基本單位把流劃分為字節流和字符流兩類以外,還可以按照流是否直接連接實際數據源,例如文件、網絡、字節數組等,將流又可以劃分為實體流和裝飾流兩大類。
其中實體流指直接連接數據源的流類,如前面介紹的FileInputStream/FileOutputStream和FileReader和FileWriter,該類流直接實現將數據源轉換為流對象,在實體流類中實現了流和數據源之間的轉換,實體流類均可單獨進行使用。
而裝飾流指不直接連接數據源,而是以其它流對象(實體流對象或裝飾流對象)為基礎建立的流類,該類流實現了將實體流中的數據進行轉換,增強流對象的讀寫能力,比較常用的有DataInputStream/DataOutputStream和BufferedReader/BufferedWriter等,裝飾流類不可以單獨使用,必須配合實體流或裝飾流進行使用。
由於裝飾流都是在已有的流對象基礎上進行創建的,所以這種創建流的方式被稱作“流的嵌套”,通過流的嵌套,可以修飾流的功能,例如使讀寫的速度增加或者提供更多的讀寫方式,方便數據格式的處理。
裝飾流不改變原來實體流對象中的數據內容,只是從實體流對象基礎上創建出的裝飾流對象相對於實體流對象進行了一些功能的增強。
流的嵌套是學習IO編程時必須掌握的知識,使用它才可以讓你真正體會到IO類設計時的設計思路,也可以方便的使用IO類。
下面分別以DataInputStream/DataOutputStream和BufferedReader/BufferedWriter為例子,詳細介紹裝飾類的使用。
11.3.3.1 DataInputStream/DataOutputStream
在前面的示例中,在向流中寫入的數據必須首先轉換為byte數組或char數組,當寫入的數據比較少、比較簡單時,則向流中寫入數據時還是不是很麻煩的,但是如果向流中寫入數據比較多時,手動轉換數據格式則會比較麻煩。當然,很多文件都是根據文件存儲的需要設計了專門的存儲格式,但是這些格式一般都比較復雜,需要閱讀專門的格式文檔才可以讀寫這些特定格式的文件。
為了簡化程序員對於流的操作,使得程序員可以從繁雜的數據格式中解脫出來,在IO類中專門設計了兩個類——DataInputStream/DataOutputStream類簡化流數據的讀寫,使用這兩個類,可以實現以增強型的讀寫方法讀寫數據,使得讀寫流的數據變得比較簡單。
在實際使用這兩個類時,必須匹配起來進行使用。也就是說,只有使用DataOutputStream流格式寫入的數據,在實際讀取時才可以使用DataInputStream進行讀取。因為在使用DataOutputStream向流中寫入數據時,除了寫入實際的數據內容以外,還寫入了特定的數據格式,該格式對於程序員來說是透明的,這種特定的格式不需要程序員熟悉,而只需要使用DataInputStream讀取即可,讀取時的順序和寫入時的順序和類型保持一致即可。
在DataInputStream類中,增加了一系列readXXX的方法,例如readInt、readUTF、readBoolean等等,而在DataOutputStream類中,也增加了一系列writeXXX的方法,例如writeInt、writeUTF、writeBoolean等等,使得對於數據的讀寫更加方便很容易。
下面以讀寫文件為例子,演示DataInputStream/DataOutputStream類的基本使用。
/**
* 模擬需要存儲到文件中的數據
* 該類中保存4種類型的數據
*/
public class MyData {
boolean b;
int n;
String s;
short sh[];
public MyData(){}
public MyData(boolean b,int n,String s,short sh[]){
this.b = b;
this.n = n;
this.s = s;
this.sh = sh;
}
}
在該示例中,需要將MyData類型的對象內部保存的數據按照一定的格式存儲到文件中,這裡列舉了2種基本數據類型boolean和int,以及兩種引用數據類型String和數組,在下面的示例代碼中將會以一定的格式寫入到文件中。
import java.io.*;
/**
* 使用DataOutputStream書寫具有一定格式的文件
*/
public class WriteFileUseDataStream {
public static void main(String[] args) {
short sh[] = {1,3,134,12};
MyData data =new MyData(true,100,"Java語言",sh);
//寫入文件
writeFile(data);
}
/**
* 將MyData對象按照一定格式寫入文件中
* @param data 數據對象
*/
public static void writeFile(MyData data){
FileOutputStream fos = null;
DataOutputStream dos = null;
try{
//建立文件流
fos = new FileOutputStream("test.my");
//建立數據輸出流,流的嵌套
dos = new DataOutputStream(fos);
//依次寫入數據
dos.writeBoolean(data.b);
dos.writeInt(data.n);
dos.writeUTF(data.s);
//寫入數組
int len = data.sh.length;
dos.writeInt(len); //數組長度
//依次寫入每個數組元素
for(int i = 0;i < len;i++){
dos.writeShort(data.sh[i]);
}
}catch(Exception e){
e.printStackTrace();
}finally{
try {
dos.close();
fos.close();
} catch (Exception e2){
e2.printStackTrace();
}
}
}
}
在該示例代碼中,首先建立一個實體流fos,該實體流連接到數據源——文件,然後以該實體流對象為基礎,使用流的嵌套,建立裝飾流對象dos,由於需要寫入流中的對象data中包含的數據比較多,所以需要以一定的格式寫入流,這裡使用DataOutputStream避免自定義數據格式,而寫入流中的順序就是該流的格式,也就是文件test.my的格式,這種格式對於程序員來說是透明的。
使用對象dos中對應的writeXXX方法依次將需要存儲的數據寫入流中,在寫入字符串時,為了使字符編碼保持一致,一般使用writeUTF寫入字符串,也就是先將字符串轉換為utf-8格式的byte數組,然後再將該數組以一定的格式寫入到流中。而在寫入數組時,則首先寫入數組的長度,然後再將數組的內容依次寫入到流中,使用這種方式就可以很方便的將數組寫入到流中。
這樣文件test.my文件就具有了自己特定的文件格式,程序員需要記憶的就是該文件在寫入時的寫入順序,可以很方便的使用DataInputStream讀取出來。
下面的代碼是使用DataInputStream讀取test.my文件的代碼,注意文件格式的處理。
import java.io.*;
/**
* 使用DataInputStream讀取自定義格式的文件
*/
public class ReadFileUseDataStream {
public static void main(String[] args) {
MyData data = readFile();
System.out.println(data.b);
System.out.println(data.n);
System.out.println(data.s);
int len = data.sh.length;
for(int i = 0;i < len;i++){
System.out.println(data.sh[i]);
}
}
/**
* 從文件test.my中讀取數據,並使用讀取到的數據初始化data對象
* @return 讀取到的對象內容
*/
public static MyData readFile(){
MyData data = new MyData();
FileInputStream fis = null;
DataInputStream dis = null;
try {
//建立文件流
fis = new FileInputStream("test.my");
//建立數據輸入流,流的嵌套
dis = new DataInputStream(fis);
//依次讀取數據,並賦值給data對象
data.b = dis.readBoolean();
data.n = dis.readInt();
data.s = dis.readUTF();
int len = dis.readInt();
data.sh = new short[len];
for(int i = 0;i < len;i++){
data.sh[i] = dis.readShort();
}
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
dis.close();
fis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return data;
}
}
在該示例代碼中,首先建立實體流fis,然後以該流對象為基礎建立dos裝飾流,然後按照寫入文件的順序,依次將流中的數據讀取出來,並將讀取到的數值賦值給data對象中對應的屬性,從而實現將數據從文件中恢復到實際的對象。
最後再次強調,DataInputStream和DataOutputStream必須匹配起來進行使用,也就是使用DataInputStream讀取的流數據必須是使用DataOutputStream流寫入的數據,這樣才能保持格式上的統一。
當然,使用DataInputStream和DataOutputStream和其它的實體流也可以匹配起來進行使用,例如和ByteArrayInputStream和ByteArrayOutputStream匹配使用將可以實現方便的把數據轉換為特定格式的byte數組以及將byte數組恢復回來,使用的格式和上面的示例類似,這裡就不再重復了。