額外功能處理流的意思是在基礎流(InputStream/OutputStream/Reader/Writer)的基礎上提供額外的功能。常見的額外功能可歸納為以下幾種。 Bufferedxxx類和Array相關的功能此處不做介紹。本文將介紹除此之外的其餘功能以及對象序列化時涉及到的序列化介面Seria ...
額外功能處理流的意思是在基礎流(InputStream/OutputStream/Reader/Writer)的基礎上提供額外的功能。常見的額外功能可歸納為以下幾種。
- 是否使用緩衝功能:BufferedInputStream/BufferedOutputStream/BufferedReader/BufferedWriter(字元流的緩衝對象還提供了操作行的方法)
- 是否串聯多個位元組輸入流:SequenceInputStream
- 是否使用對象序列化功能:ObjectInputStream/ObjectOutputStream(涉及序列化介面Serializable)
- 是否讓流保證數據類型不變:DataInputStream/DataOutputStream
- 是否讓輸出流輸出時保證輸出字面符號:PrintStream/PrintWriter(列印流)
- 是否要操作記憶體中的字元串和數組:ByteArrayInputStream/ByteArrayOutputStream/CharArrayReader/CharArrayWriter
Bufferedxxx類和Array相關的功能此處不做介紹。本文將介紹除此之外的其餘功能以及對象序列化時涉及到的序列化介面Serializable。
1.輸入流的串聯:SequenceInputStream
SequenceInputStream按照IO體系命名的特點來理解,大致是"將位元組輸入流存放到Sequence序列中",實際上,它可用來串聯多個輸入流。意思是:有輸入流1、輸入流2、輸入流3,原本的行為是按照順序先後讀取輸入流1、2、3,現在將這3個輸入流按順序連起來當作一個大輸入流,直到輸入流3讀完後才到流的末尾。
這個序列輸入流類在IO體系裡有點特立獨行,它只有輸入流,沒有對應的輸出流。它的作用是以操作一個輸入流的方式來將多個輸入流按序追加讀取。例如,將多個文件的數據以追加的方式寫入到一個目標文件中。
當調用SequenceInputStream的close()方法時,它將會自動關閉所有它所串聯的輸入流。
如下圖:
要使用SequenceInputStream,首先看構造方法SequenceInputStream(Enumeration<? extends InputStream> e)
,可見它只能接收枚舉出來的位元組輸入流。但如何獲取到這些枚舉元素?可以將各個輸入流存放到一個集合中,然後使用Collections工具類中的enumeration(Collection c)方法將這個集合轉換為Enumeration對象。在此還需說明的是,通常SequenceInputStream要串聯的多個流都是有先後順序的,例如1.txt,2.txt,3.txt依序串聯下去,所以枚舉時也要保證能夠依序枚舉出來,這也要求在Collection轉換為Enumeration時,集合中的流對象在集合中也是有序的,這意味著使用List集合來存儲這些流對象是最佳的。
例如,下麵的示例中將{1..6}.txt共6個txt文件按文件名排序先後串聯成一個SequenceInputStream。
//存儲多個位元組輸入流對象到List集合中
List<FileInputStream> list = new ArrayList<FileInputStream>();
for(int i=1;i<=6;i++){
list.add(new FileInputStream(i+".txt"));
}
//將List集合轉換為枚舉對象Enumeration
Enumeration<FileInputStream> en = Collections.enumeration(list);
//將枚舉出來的各個位元組輸入流串聯起來
SequenceInputStream sis = new SequenceInputStream(en);
2. ObjectInputStream/ObjectOutputStream和序列化介面Serializable
輸入流和輸出流可以按位元組、存儲讀取媒體類、文本類文件,但能否將java中的對象也作為數據持久化到文件中呢?io包中提供了ObjectInputStream和ObjectOutputStream來讀、寫對象。
例如給定如下Student類,將以此類作為ObjectInputStream/ObjectOutputStream流讀、寫的對象。
class Student {
String name;
int age;
Student(String name,int age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String toString(){
return "{name="+name+",age="+age+"}";
}
}
下麵使用ObjectOutputStream將Student對象寫入到文件中,這類文件的規範尾碼名為".object"。該類的構造方法為ObjectOutputStream(OutputStream out)
。
import java.io.*;
import java.util.*;
public class ObjectStream {
public static void main(String[] args) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/temp/a.txt"));
Student stu = new Student("malongshuai",22);
oos.writeObject(stu);
}
}
編譯並執行上述代碼,將拋出NotSerializableException
異常,意思是未序列化。那麼誰沒有序列化?Student對象。要想讓某對象序列化的方式很簡單,只需讓Student類實現Serializable介面即可。如下:
class Student implements Serializable {
String name;
int age;
Student(String name,int age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String toString(){
return "{name="+name+",age="+age+"}";
}
}
可以使用ObjectInputStream從文件中讀取曾被序列化的數據。這稱為"反序列化"。
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/temp/a.txt"));
Object obj = ois.readObject();
System.out.println("read Object from file:"+stu1.toString()); //多態
看上去序列化和反序列化是一件很簡單的事情。確實如此,但其有不少知識點和需要註意的關鍵點。
- 什麼是序列化?
從前面的例子中可以看出,序列化的方式非常簡單,只需實現Serializable介面即可。它不提供任何方法。序列化的意義僅僅只是為類進行一種特殊的標識,即所謂的"蓋戳"。就像夫妻如何證明他們是夫妻,頒發一個結婚證即可,再例如豬肉憑什麼是合格的?給它貼一張合格標簽即可。 - 序列化的目的是什麼?
為了將某些對象持久化保存起來,供以後反序列化的時候讀取。 - 序列化後的對象在存儲時會存儲哪些數據?
存儲的內容包括:類名和類簽名(類的序列化版本號SerialVersionUID)、對象的欄位值和數組值,以及從初始對象中引用的其他所有對象的閉包。大致可以看作是存儲了一個類的版本號、類名、某些欄位的值以及引用的對象。當然,並非所有欄位值都會存儲,見下文的第6點。 - 對某個對象序列化後,修改對象的屬性(例如將成員變數的修飾符從public改為private),反序列化時將會如何?
因為保存起來的序列化數據帶有一個類簽名SerialVersionUID,而修改類的定義後,編譯時這個類會生成Class文件,而這個文件中的版本號將不再和之前序列化時保存的版本號相同。於是拋出異常。
由此可以明確一點:class文件中僅只存儲類的定義語句,在new對象時將在堆記憶體中開闢一段空間並存儲對象數據(如成員變數)。反序列化實際上是將保存起來的類對象數據載入到這個開闢出來的對象空間中。java.io.InvalidClassException: Student; local class incompatible: stream classdesc serialVersionUID = -9151998530267376490, local class serialVersionUID = -3521625297801190192
- 如何保證反序列化的成功?
強烈建議顯式在實現了Serializable介面的類中,聲明一個固定的序列化。如:
這樣一來,無論是序列化保存時,還是後來修改了類定義生成的class文件中,其版本號都是固定且相同的,也就是說不會因為序列化版本號不同而反序列化失敗。public/private/... static final long serialVersionUID = 123456L;
- 類中所有屬性都應該序列化嗎?
顯然不是。有兩類數據不會被保存:靜態變數(static)、瞬態變數(transient)。例如密碼欄位、時間點等隨時改變、有安全隱患的數據不應該被序列化保存。在進行序列化的時候,只是將堆記憶體中的數據保存起來,所以加了static關鍵字的靜態屬性不會被序列化。而加了transient關鍵字的(如為Student類的age加上瞬態屬性public transient int age;
)也不會被序列化。
看上去說了一大堆,其實操作起來非常簡單,只需為待序列化的對象實現Serializable介面並聲明serialVersionUID就可以了。
以下是ObjectInputStream和ObjectOutputStream序列化、反序列化多個對象的示例。序列化的時候使用了集合的方式,將多個Student對象存儲到集合中,然後遍歷集合來序列化各個Student對象。反序列化的時候,由於ObjectInputStream的readObject()一次讀取一個對象示例的數據,且沒有提供合適的判斷流結尾的返回值,只是在讀取到結尾時會拋出EOFException異常。因此此處採用while無限迴圈的方式,並通過拋出的EOFException異常來結束迴圈。
import java.io.*;
import java.util.*;
public class ObjectStream {
public static void main(String[] args) {
//將各學生對象存放到集合中
List<Student> list = new ArrayList<Student>();
list.add(new Student("Malongshuai",22));
list.add(new Student("Gaoxiaofang",22));
//序列化
//writeObj(list,"d:/temp/a.object");
//反序列化
readObj("d:/temp/a.object");
}
//序列化
public static void writeObj(List list,String filename) {
//遍歷集合中的對象並將它們序列化
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(filename));
for(Iterator it = list.iterator();it.hasNext();) {
oos.writeObject(it.next());
}
} catch (FileNotFoundException f) {
f.printStackTrace();
} catch (IOException i) {
i.printStackTrace();
} finally {
if(oos!=null) {
try {
oos.close();
} catch(IOException i){
i.printStackTrace();
}
}
}
}
//反序列化:讀取序列化數據
public static void readObj(String filename) {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(filename));
while(true) {
Student student = (Student)ois.readObject();
System.out.println(student.toString());
}
} catch (EOFException e){
} catch (FileNotFoundException f) {
f.printStackTrace();
} catch (IOException i) {
i.printStackTrace();
} catch (ClassNotFoundException c){
c.printStackTrace();
} finally {
if(ois!=null) {
try {
ois.close();
} catch (IOException i) {
i.printStackTrace();
}
}
}
}
}
class Student implements Serializable {
static final long serialVersionUID = 123456l;
String name = "hello";
public int age;
Student(String name,int age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String toString(){
return "{name="+name+",age="+age+"}";
}
}
3.PrintStream/PrintWriter
首先看一個容易出現疑惑的現象。
import java.io.*;
public class DataStream {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("d:/temp/b.txt");
fos.write(97);
fos.write(353);
fos.close();
FileInputStream fis = new FileInputStream("d:/temp/b.txt");
byte[] buf = new byte[10];
int len = 0;
while((len=fis.read(buf))!=-1) {
String str = new String(buf);
System.out.println(str);
}
fis.close();
}
}
上面的代碼中,向文件中寫入的是數值97和353,但無論是用記事本解析還是從這裡讀取到的結果都是"aa"共兩個位元組的字母。為什麼會如此?
在write(Int i)方法寫入數據時,它會將最低位位元組寫入,而忽略前三個位元組。例如97的二進位碼為"00000000 00000000 00000000 01100001",忽略前三個位元組,寫入到文件中的二進位數據就只剩下"01100001",而這被讀取或被解析時正好解析為字母a。同理353,它的二進位數據為"00000000 00000000 00000001 01100001",雖然第三個位元組最後一位為1,但它還是被忽略,導致寫入到文件中的二進位數據仍然為"01100001",解析後就是字母a。
要避免這種問題,可以使用位元組列印流PrintStream或字元列印流PrintWriter,它會將數據按照字面展現形式輸出。例如下麵的例子中,將會向文件中分別寫入"a97"。
FileOutputStream fos = new FileOutputStream("d:/temp/a.txt");
PrintStream ps = new PrintStream(fos);
//PrintStream ps = new PrintStream("d:/temp/a.txt")
ps.write(97); //它調用的其實還是fos.write(),所以仍然存儲字母a
ps.print(97); //存儲字面符號97
ps.close();
使用println()可以換行,使用printf()可以以C語言的列印格式輸出。
另外,在PrintWriter中(不包括PrintStream),有一個自動更新autoFlush的概念,它表示每輸出一次換行符就自動flush一次。但註意,PrintWriter的自動刷新只對println()和printf()方法有效,對print()無效。之所以不包括PrintStream,是因為PrintWriter因為字元集處理的原因在輸出的時候涉及了一個額外的緩衝區,自動刷新就是將此緩衝區的數據flush,而PrintStream則沒有這個額外的緩衝區,因此它是實時輸出的。
4.DataInputStream/DataOutputStream
還是前面的問題,如何將97作為int數據類型保存到文件中(即將4個位元組的97存到文件中)。也就是保證數據的數據類型不變。使用DataInputStream/DataOutputStream即可。
DataOutputStream dos = new DataOutputStream(new FileOutputStream("d:/temp/a.txt"));
dos.writeInt(97);
DataInputStream dis = new DataInputStream(new FileInputStream("d:/temp/a.txt"));
System.out.println(dis.readInt());
這樣將會把97的4個位元組存儲到文件中,如果使用記事本去解析,得到的結果會是" a",共4個位元組,雖然得到的結果是a,但這隻是用記事本解析的而已。使用上面的readInt()讀取的結果則是正確的。
註:若您覺得這篇文章還不錯請點擊右下角推薦,您的支持能激發作者更大的寫作熱情,非常感謝!