本文着重講解一下Java序列化的相關內容。
如果對Java序列化感興趣的同學可以研究一下。
一.Java序列化的作用
有的時候我們想要把一個Java對象變成字節流的形式傳出去,有的時候我們想要從一個字節流中恢復一個Java對象。例如,有的時候我們想要
把一個Java對象寫入到硬盤或者傳輸到網路上面的其它計算機,這時我們就需要自己去通過java把相應的對象寫成轉換成字節流。對於這種通用
的操作,我們為什么不使用統一的格式呢?沒錯,這里就出現了java的序列化的概念。在Java的OutputStream類下面的子類ObjectOutput-
Stream類就有對應的WriteObject(Object object) 其中要求對應的object實現了java的序列化的接口。
為了更好的理解java序列化的應用,我舉兩個自己在開發項目中遇到的例子:
1)在使用tomcat開發JavaEE相關項目的時候,我們關閉tomcat后,相應的session中的對象就存儲在了硬盤上,如果我們想要在tomcat重啟的
時候能夠從tomcat上面讀取對應session中的內容,那么保存在session中的內容就必須實現相關的序列化操作。
2)如果我們使用的java對象要在分布式中使用或者在rmi遠程調用的網絡中使用的話,那么相關的對象必須實現java序列化接口。
親愛的小伙伴,大概你已經了解了java序列化相關的作用,接下來們來看看如何實現java的序列化吧。~
二.實現java對象的序列化和反序列化。
Java對象的序列化有兩種方式。
a.是相應的對象實現了序列化接口Serializable,這個使用的比較多,對於序列化接口Serializable接口是一個空的接口,它的主要作用就是
標識這個對象時可序列化的,jre對象在傳輸對象的時候會進行相關的封裝。這里就不做過多的介紹了。
下面是一個實現序列化接口的Java序列化的例子:非常簡單
package com.shop.domain;
import java.util.Date;
public class Article implements java.io.Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
private String title; //文章標題
private String content; // 文章內容
private String faceIcon;//表情圖標
private Date postTime; //文章發表的時間
private String ipAddr; //用戶的ip
private User author; //回復的用戶
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getFaceIcon() {
return faceIcon;
}
public void setFaceIcon(String faceIcon) {
this.faceIcon = faceIcon;
}
public Date getPostTime() {
return postTime;
}
public void setPostTime(Date postTime) {
this.postTime = postTime;
}
public User getAuthor() {
return author;
}
public void setAuthor(User author) {
this.author = author;
}
public String getIpAddr() {
return ipAddr;
}
public void setIpAddr(String ipAddr) {
this.ipAddr = ipAddr;
}
}
b.實現序列化的第二種方式為實現接口Externalizable,Externlizable的部分源代碼如下:
* @see java.io.ObjectInput
* @see java.io.Serializable
* @since JDK1.1
*/
public interface Externalizable extends java.io.Serializable {
/**
* The object implements the writeExternal method to save its contents
* by calling the methods of DataOutput for its primitive values or
沒錯,Externlizable接口繼承了java的序列化接口,並增加了兩個方法:
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
首先,我們在序列化對象的時候,由於這個類實現了Externalizable 接口,在writeExternal()方法里定義了哪些屬性可以序列化,
哪些不可以序列化,所以,對象在經過這里就把規定能被序列化的序列化保存文件,不能序列化的不處理,然后在反序列的時候自動調
用readExternal()方法,根據序列順序挨個讀取進行反序列,並自動封裝成對象返回,然后在測試類接收,就完成了反序列。
所以說Exterinable的是Serializable的一個擴展。
為了更好的理解相關內容,請看下面的例子:
package com.xiaohao.test;
import java.io.Externalizable;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 測試實體類
* @author 小浩
* @創建日期 2015-3-12
*/
class Person implements Externalizable{
private static final long serialVersionUID = 1L;
String userName;
String password;
String age;
public Person(String userName, String password, String age) {
super();
this.userName = userName;
this.password = password;
this.age = age;
}
public Person() {
super();
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
/**
* 序列化操作的擴展類
*/
@Override
public void writeExternal(ObjectOutput out) throws IOException {
//增加一個新的對象
Date date=new Date();
out.writeObject(userName);
out.writeObject(password);
out.writeObject(date);
}
/**
* 反序列化的擴展類
*/
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
//注意這里的接受順序是有限制的哦,否則的話會出錯的
// 例如上面先write的是A對象的話,那么下面先接受的也一定是A對象...
userName=(String) in.readObject();
password=(String) in.readObject();
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
Date date=(Date)in.readObject();
System.out.println("反序列化后的日期為:"+sdf.format(date));
}
@Override
public String toString() {
//注意這里的年齡是不會被序列化的,所以在反序列化的時候是讀取不到數據的
return "用戶名:"+userName+"密 碼:"+password+"年齡:"+age;
}
}
/**
* 序列化和反序列化的相關操作類
* @author 小浩
* @創建日期 2015-3-12
*/
class Operate{
/**
* 序列化方法
* @throws IOException
* @throws FileNotFoundException
*/
public void serializable(Person person) throws FileNotFoundException, IOException{
ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream("a.txt"));
outputStream.writeObject(person);
}
/**
* 反序列化的方法
* @throws IOException
* @throws FileNotFoundException
* @throws ClassNotFoundException
*/
public Person deSerializable() throws FileNotFoundException, IOException, ClassNotFoundException{
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("a.txt"));
return (Person) ois.readObject();
}
}
/**
* 測試實體主類
* @author 小浩
* @創建日期 2015-3-12
*/
public class Test{
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
Operate operate=new Operate();
Person person=new Person("小浩","123456","20");
System.out.println("為序列化之前的相關數據如下:\n"+person.toString());
operate.serializable(person);
Person newPerson=operate.deSerializable();
System.out.println("-------------------------------------------------------");
System.out.println("序列化之后的相關數據如下:\n"+newPerson.toString());
}
}
首先,我們在序列化UserInfo對象的時候,由於這個類實現了Externalizable 接口,在writeExternal()方法里定義了哪些屬性可
以序列化,哪些不可以序列化,所以,對象在經過這里就把規定能被序列化的序列化保存文件,不能序列化的不處理,然后在反序列
的時候自動調用readExternal()方法,根據序列順序挨個讀取進行反序列,並自動封裝成對象返回,然后在測試類接收,就完成了反
序列。
***對於實現Java的序列化接口需要注意一下幾點:
1.java中的序列化時transient變量(這個關鍵字的作用就是告知JAVA我不可以被序列化)和靜態變量不會被序列
化(下面是一個測試的例子)
import java.io.*;
class Student1 implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient String password;
private static int count = 0;
public Student1(String name, String password) {
System.out.println("調用Student的帶參的構造方法");
this.name = name;
this.password = password;
count++;
}
public String toString() {
return "人數: " + count + " 姓名: " + name + " 密碼: " + password;
}
}
public class ObjectSerTest1 {
public static void main(String args[]) {
try {
FileOutputStream fos = new FileOutputStream("test.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
Student1 s1 = new Student1("張三", "12345");
Student1 s2 = new Student1("王五", "54321");
oos.writeObject(s1);
oos.writeObject(s2);
oos.close();
FileInputStream fis = new FileInputStream("test.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Student1 s3 = (Student1) ois.readObject();
Student1 s4 = (Student1) ois.readObject();
System.out.println(s3);
System.out.println(s4);
ois.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e1) {
e1.printStackTrace();
}
}
}
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Test{
public static void main(String args[]){
try {
FileInputStream fis = new FileInputStream("test.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Student1 s3 = (Student1) ois.readObject();
Student1 s4 = (Student1) ois.readObject();
System.out.println(s3);
System.out.println(s4);
ois.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e1) {
e1.printStackTrace();
}
}
}
2.也是最應該注意的,如果你先序列化對象A后序列化B,那么在反序列化的時候一定記着JAVA規定先讀到的對象
是先被序列化的對象,不要先接收對象B,那樣會報錯.尤其在使用上面的Externalizable的時候一定要注意讀取
的先后順序。
3.實現序列化接口的對象並不強制聲明唯一的serialVersionUID,是否聲明serialVersionUID對於對象序列化的向
上向下的兼容性有很大的影響。我們來做個測試:
思路一
把User中的serialVersionUID去掉,序列化保存。反序列化的時候,增加或減少個字段,看是否成功。
保存到文件中:
增加或者減少字段后,從文件中讀出來,反序列化:
結果:拋出異常信息
Java代碼
思路二
eclipse指定生成一個serialVersionUID,序列化保存,修改字段后反序列化
略去代碼
結果:反序列化成功
結論
如果沒有明確指定serialVersionUID,序列化的時候會根據字段和特定的算法生成一個serialVersionUID,當屬性有變化時這個id發生了變化,所以反序列化的時候
就會失敗。拋出“本地classd的唯一id和流中class的唯一id不匹配”。
jdk文檔關於serialVersionUID的描述:
寫道
三.實現序列化的其它方式 (這是一個擴展內容,感興趣的可以擴展一下)
1)是把對象包裝成JSON字符串傳輸。
這里采用JSON格式同時使用采用Google的gson-2.2.2.jar 進行轉義
2)采用谷歌的ProtoBuf
隨着Google工具protoBuf的開源,protobuf也是個不錯的選擇。對JSON,Object Serialize(Java的序列化和反序列化),
ProtoBuf 做個對比。
定義一個通用的待傳輸的對象UserVo:
初始化User的實例src:
得到的字符串:
字節數為153
Json的優點:明文結構一目了然,可以跨語言,屬性的增加減少對解析端影響較小。缺點:字節數過多,依賴於不同的第三方類庫。
Object Serialize(Java的序列化和反序列化)
UserVo實現Serializalbe接口,提供唯一的版本號:
序列化方法:
字節數是238
反序列化:
Object Serializalbe 優點:java原生支持,不需要提供第三方的類庫,使用比較簡單。
缺點:無法跨語言,字節數占用比較大,某些情況下對於對象屬性的變化比較敏感。
對象在進行序列化和反序列化的時候,必須實現Serializable接口,但並不強制聲明唯一的serialVersionUID
是否聲明serialVersionUID對於對象序列化的向上向下的兼容性有很大的影響。
Google ProtoBuf
protocol buffers 是google內部得一種傳輸協議,目前項目已經開源(http://code.google.com/p/protobuf/)。
它定義了一種緊湊得可擴展得二進制協議格式,適合網絡傳輸,並且針對多個語言有不同得版本可供選擇。
以protobuf-2.5.0rc1為例,准備工作:
下載源碼,解壓,編譯,安裝
測試:
安裝成功!
進入源碼得java目錄,用mvn工具編譯生成所需得jar包,protobuf-java-2.5.0rc1.jar
1、編寫.proto文件,命名UserVo.proto
2、在命令行利用protoc 工具生成builder類
得到UserProtos類
3、編寫序列化代碼
字節數53
反序列化
結果:tmac,反序列化成功google protobuf 優點:字節數很小,適合網絡傳輸節省io,跨語言 。
缺點:需要依賴於工具生成代碼。
工作機制
proto文件是對數據的一個描述,包括字段名稱,類型,字節中的位置。protoc工具讀取proto文件生成對應builder代碼的類庫。protoc xxxxx --java_out=xxxxxx 生成java類庫。builder類根據自己的算法把數據序列化成字節流,或者把字節流根據反射的原理反序列化成對象。官方的示例:https://developers.google.com/protocol-buffers/docs/javatutorial。
proto文件中的字段類型和java中的對應關系:
詳見:https://developers.google.com/protocol-buffers/docs/proto
| .proto Type | java Type | c++ Type |
| double | double | double |
| float | float | float |
| int32 | int | int32 |
| int64 | long | int64 |
| uint32 | int | uint32 |
| unint64 | long | uint64 |
| sint32 | int | int32 |
| sint64 | long | int64 |
| fixed32 | int | uint32 |
| fixed64 | long | uint64 |
| sfixed32 | int | int32 |
| sfixed64 | long | int64 |
| bool | boolean | bool |
| string | String | string |
| bytes | byte | string |
| 方式 | 優點 | 缺點 |
| JSON | 跨語言、格式清晰一目了然 |
字節數比較大,需要第三方類庫 |
| Object Serialize | java原生方法不依賴外部類庫 | 字節數比較大,不能跨語言 |
| Google protobuf | 跨語言、字節數比較少 |
編寫.proto配置用protoc工具生成對應的代碼 |
