1.Properties類
- 該類是一個Map集合。之前學習的集合都是將數據存儲到內存中,但是這個集合類它可以和IO流結合,直接把集合中的數據保存在硬盤的文件中,或者直接從硬盤的文件中加載數據保存在集合中;
- 這個集合中的鍵和值都是String類型的字符串;
- 這個集合類沒有泛型;
1.1 特有函數(功能)
-
Object
setProperty(String key, String value)
添加元素,等同於Map中的put功能。
返回:屬性列表中指定鍵的舊值,如果沒有值,則爲 null。 -
String
getProperty(String key)
根據鍵找值,等同於Map中的 get(Object key)。
返回:屬性列表中具有指定鍵值的值。
Set stringPropertyNames() 獲取所有鍵的集合,等同於Map中的keySet方法。
//遍歷演示
public class PropertiesDemo2 {
public static void main(String[] args) {
//創建集合對象
Properties prop = new Properties();
//向集合中添加數據
prop.setProperty("張三","北京");
prop.setProperty("李四","上海");
prop.setProperty("王五","南京");
prop.setProperty("田七","杭州");
//使用對象調用stringPropertyNames()函數獲取所有的鍵集合
Set<String> keys = prop.stringPropertyNames();
//遍歷集合
for (Iterator<String> it = keys.iterator(); it.hasNext();) {
String key = it.next();
//根據key獲得value
String value = prop.getProperty(key);
System.out.println(key+"---"+value);
}
}
}
1.2 Properties與流相關的操作
Properties類與流相互操作的主要方法如下所示:
1.把集合中的數據通過流保存到文件中。
public static void method_1() throws IOException {
//創建集合對象
Properties prop = new Properties();
//向集合中添加數據
prop.setProperty("張三","北京");
prop.setProperty("李四","上海");
prop.setProperty("王五","南京");
prop.setProperty("田七","杭州");
//創建輸出流對象
FileOutputStream fos = new FileOutputStream("D:\\person.txt");
//使用集合對象prop調用store()函數向文件中添加數據
/*
* 如果在這裏使用了輸出流,那麼不用關閉資源,在Properties類的store函數中
* 已經幫助我們關閉了
*/
prop.store(fos, "person");
}
上述代碼運行的結果是:
說明:
1)上述結果中的 # 表示註釋,#號後面的內容如果從硬盤文件中加載到內存中是不會被讀取的;
2)在Java中漢字字符都是以gbk來保存的,而當你使用字節流FileOutputStream 進行寫數據的時候底層使用的是ISO-8859-1來進行解碼
的時候,所以保存在硬盤文件中都看不懂;
向硬盤中寫數據使用字符流,不使用字節流,因爲字符流Writer底層使用的編碼表是GBK。
//把Properties集合中的數據寫到本地文件中
public static void method_1() throws IOException {
//創建集合對象
Properties prop = new Properties();
//向集合中添加數據
prop.setProperty("張三","北京");
prop.setProperty("李四","上海");
prop.setProperty("王五","南京");
prop.setProperty("田七","杭州");
//使用集合對象prop調用store()函數向文件中添加數據
/*
* 如果在這裏使用了輸出流,那麼不用關閉資源,在Properties類的store函數中
* 已經幫助我們關閉了
*/
prop.store(new FileWriter("D:\\person.txt"), "person");
}
結果如下:
2.從流中把數據加載到集合中。
public static void main(String[] args) throws IOException {
Properties prop = new Properties();
//使用集合對象prop調用load()函數從硬盤上加載數據
//prop.load(new FileInputStream("D:\\person.txt"));
prop.load(new FileReader("D:\\person.txt"));
//遍歷集合
Set<String> keys = prop.stringPropertyNames();
for (Iterator<String> it = keys.iterator(); it.hasNext();) {
String key = it.next();
//根據key鍵獲得value
String value = prop.getProperty(key);
System.out.println(key+"---"+value);
}
}
注意:
1)如果將內存中的數據寫到硬盤上的文件中使用字節輸出流,那麼最好從硬盤中向內存中讀取也使用字節輸入流;
2)如果將內存中的數據寫到硬盤上的文件中使用字符輸出流,那麼最好從硬盤中向內存中讀取也使用字符輸入流;
兩個流一定要對應。
2.序列化流與反序列化流
2.1 序列化
序列化和反序列化流:它們的功能是可以把程序中new出來的對象保存到持久設備上,或者從持久設備上讀取被保存的對象。
1.序列化
序列化:把創建出來的對象(new出來的對象),以及對象中的成員變量的數據轉化爲字節數據,寫到流中,然後存儲到硬盤的文件中。
使用場景:
在創建對象並給所創建的對象賦予了值後,當前創建出來的對象是存放在堆內存中的,當JVM停止後,堆中的對象也被釋放了,如果下一次想要繼續使用之前的對象,需要再次創建對象並賦值。
然而使用序列化對象,就可以把創建出來的對象及對象中數據存放到硬盤的文件中,下次使用的時候不用在重新賦值,而是直接讀取使用即可。
public class Student implements Serializable {
//屬性
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class ObjectOutputStreamDemo {
public static void main(String[] args) throws IOException {
//創建學生對象
Student s = new Student("黑旋風",18);
//把創建出來的學生對象持久化保存在硬盤中
//創建序列化對象 創建輸出流對象並關聯目標文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\s.txt"));
//使用序列化對象中的方法持久化學生對象
oos.writeObject(s);
//關閉資源
oos.close();
}
}
注意,如果類沒有繼承Serializable接口
分析異常:
上述異常表示對象需要實現序列化接口時,而對象沒有實現序列化接口,就會拋出該異常。
換句話說當我們需要把Java中的一個對象(例如Student類的對象)持久的保存在硬盤上的文件中的時候,這時這個對象所屬的類需要實現一個接口
,如果對象所屬的類不實現該接口,就報上述的異常。
那麼這個接口到底叫什麼呢?其實在序列化類ObjectOutputStream的API中已經有所描述,如下圖
接口:序列化接口。Serializable:
它是一個標記性接口。這個接口中沒有任何的方法,這種接口稱爲標記型接口!它僅僅是一個標識。是一個標記接口爲了啓動一個序列化功能。只有具備了這個接口標識的類才能通過Java中的序列化和反序列化流操作這個對象。
注意:只要一個類實現了Serializable接口,那麼都會給每個實現類分配一個序列版本號作爲唯一標識。
小結:
當對創建的對象進行序列化操作時,必須保證對象所屬的類要實現序列化接口Serializable。
2.反序列化
反序列化:可以把序列化後的對象(硬盤上的文件中的對象數據),讀取到內存中,然後就可以直接使用對象。這樣做的好處是不用再一次創建對象了,直接反序列化就可以了。
反序列化對象所屬的類是ObjectInputStream類:
public class ObjectInputStreamDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//創建反序列化對象,指定一個字節輸入流用來讀取持久文件
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\s.txt"));
//使用反序列化對象ois調用函數進行讀取數據
Student s = (Student) ois.readObject();
System.out.println(s.getName()+"==="+s.getAge());
//關閉資源
ois.close();
}
}
我們對Student類做了一些簡單的修改,無關緊要的修改。例如給Student類添加一個屬性字段或者函數都可以,再次反序列化,就出問題了,報如下圖所示的異常:
通過以上發生的異常我們發現是由於readObject()函數發生的異常,所以我們接下來會查看readObject()函數。
接下來查看InvalidClassException 異常類:
問題一:什麼是該類的序列版本號呢?
類要進行序列化操作時,需要實現Serializable接口。(Serializable接口也稱爲標記接口),實現了標記接口的類,該類會存在一個標記值。這個標記值就是該類的序列版本號。這個版本號和該類相關聯。
說明:也就是說只要一個類實現Serializable接口,那麼在編譯源文件時,生成的class文件中就會生成一個和該類相關聯的序列版本號。
問題二:這個版本號有什麼作用呢?
在序列化時,這個版本號會隨着對象一起被序列化到本地文件中。在反序列化的時候,使用流從硬盤上會讀取之前序列化的文件,那麼jvm會拿着使用流讀取到的序列化的版本號和本地硬盤上即class文件中的序列化版本號進行匹配,如果不匹配就會拋異常。Java.io.InvalidClassException。
所以可以理解這個序列化版本號serialVersionUID是用來驗證的,防止反序列化的對象和本地的類不匹配。
問題三:這個序列化版本號是如何生成的呢?
由於實現類已經實現了Serializable接口,那麼只要重新編譯源文件的時候,編譯器就會根據類的各個方面(如類的成員變量、成員函數、修飾符、函數返回值類型等)計算成爲該類的默認 serialVersionUID 值(版本號)
。
serialVersionUID 稱爲序列版本編號(標記值)。
通過以上分析,可以得到一個結論:
如果可以保證反序列化對象和序列化對象的標記值相同,就可以避免異常的發生。
那麼我們如何做才能保證反序列化對象和序列化對象的標記值相同呢?
我們修改Student類是無關緊要的。在我們修改Student類的時候,我們不希望它拋異常。我們可以給類定義一個默認的版本號,即給Student類添加標記值也就是版本號serialVersionUID。這樣一來,添加的標記值即版本號會隨着對象的序列化持久保存。無論是序列化,還是反序列化,都不會再根據類的各個方面計算版本號了。序列化和反序列化的版本號會永遠一致,所以不會拋出異常,這樣就可以避免InvalidClassException異常的發生了。
但是,這樣一來,類的安全問題,只能自己來維護。
因爲已經將類的對象序列化之後,由於類中已經顯示定義了版本號,那麼反序列化的時候即使修改了Student類,也不會報異常了。
注意:
在使用序列化操作時,不是所有的成員的值都可以進行序列化操作:
- 靜態成員的值不會進行序列化操作;
- 瞬態成員的值也不會進行序列化操作;
瞬態成員:在進行序列化操作時,如果希望某些成員不被序列化,而該成員又不能是靜態成員(不希望隨着類加載而存在,和對象有關係),就使用關鍵字transient(表示瞬時的意思)把成員變爲瞬態成員。在被反序列化後,transient 變量的值被設爲初始值,如 int 型的是 0,對象型的是 null。
代碼如下所示:
說明:由於name和age屬性分別被靜態和瞬態修飾了,所以他們的值都不能被序列化到硬盤上,所以反序列化都是默認值。
總結,記住:
1、當一個對象需要被序列化 或 反序列化的時候對象所屬的類需要實現Serializable接口。
2、被序列化的類中需要添加一個serialVersionUID。
序列化的細節:
序列化的時候,只能把對象在堆中的所有數據持久保存到持久設備上。靜態的成員變量不會被序列化。
有時我們在序列化的時候某些非靜態成員變量也不想被序列化的時候,我們可以使用瞬態關鍵字(transient)修飾。
3.打印流
打印流:用來向文件、控制檯、網絡中打印數據的流。
打印流有兩種:
- PrintStream:打印字節數據
- PrintWriter:打印字符數據
1.PrintStream
public static void main(String[] args) throws FileNotFoundException {
System.out.print("hello world");
/*
* System表示一個系統類
* System.out的返回值是一個打印流PrintStream
* 這個打印流的目的地就是控制檯
*/
PrintStream ps = System.out;
//使用PrintStream打印流對象調用函數輸出數據
ps.print("java");
ps.print("鎖哥");
//向文件中打印數據
PrintStream ps2 = new PrintStream("E:\\ps.txt");
ps2.print(true);//true
ps2.println("hello");
ps2.println("world");
ps2.println("java");
// 釋放資源
ps.close();
}
2.PrintWriter
輸出結果:
pw.txt文件中沒有任何數據,因爲沒有刷新流中的數據。
開啓自動刷新的打印流:
上述代碼中已經開啓了自動刷新功能,可是依然沒有將數據寫到文件中,爲什麼?
原因:PrintWriter流只有在調用println()、printf()、format()三個方法中任意一個方法時,纔會自動刷新
總結:
在使用打印流的時候,需要啓動的自動刷新之後,才能針對println() , format(), printf():完成刷新的功能。
如果及時啓動自動刷新,但是沒有調用上述的這三個方法,依然不會刷新。
其實在上述代碼中直接使用字節打印流PrintWriter的對象pw調用close()或者flush()函數都會將數據寫到文件中;
打印流特點:
A:只有輸出流,沒有輸入流。
B:既可以輸出到文件,又可以輸出到其它流中。
C:會把一切數據當成字符串來輸出。
D:打印流打印char數組時會輸出數組中的內容,打印其它數組會打印地址值。
E:PrintStream自帶自動刷新,PrintWriter需要啓動自動刷新。