文章1:https://www.hollischuang.com/archives/1150
文章2:http://www.hollischuang.com/archives/1140#What%20Serializable%20Did
這里通過(guò)分析數(shù)組列表的序列化來(lái)介紹Java序列化的相關(guān)內(nèi)容达吞。主要涉及到以下幾個(gè)問(wèn)題:序列化是一種對(duì)象持久化的手段。
怎么實(shí)現(xiàn)Java的序列化
為什么實(shí)現(xiàn)了java.io.Serializable接口才能被序列化
瞬變的作用是什么
怎么自定義序列化策略
自定義的序列化策略是如何被調(diào)用的
ArrayList對(duì)序列化的實(shí)現(xiàn)有什么好處
Java對(duì)象的序列化
Java平臺(tái)允許我們?cè)趦?nèi)存中創(chuàng)建可替換的Java對(duì)象本涕,但一般情況下烦感,只有當(dāng)JVM在運(yùn)行時(shí)铅檩,這些對(duì)象才可能存在仗颈,即刻盐,這些對(duì)象的生命周期不會(huì)比JVM的生命周期延長(zhǎng)尚困。但在現(xiàn)實(shí)應(yīng)用中蠢箩,就可能要求在JVM停止運(yùn)行之后才能保存(持久化)指定的對(duì)象,并在將來(lái)重新讀取被保存的對(duì)象尾组。Java對(duì)象序列化就能夠幫助我們實(shí)現(xiàn)該功能忙芒。
使用Java對(duì)象序列化示弓,在保存對(duì)象時(shí)讳侨,會(huì)把其狀態(tài)保存為一個(gè)字節(jié),在未來(lái)奏属,再將這些字節(jié)組裝成對(duì)象跨跨。必須注意地是,對(duì)象序列化保存的是對(duì)象的“狀態(tài)” 囱皿,即它的成員變量勇婴。可以可知嘱腥,對(duì)象序列化不會(huì)關(guān)注類中的靜態(tài)變量耕渴。
除了在持久化對(duì)象時(shí)會(huì)用到對(duì)象序列化之外,當(dāng)使用RMI(遠(yuǎn)程方法調(diào)用)齿兔,或在網(wǎng)絡(luò)中傳遞對(duì)象時(shí)橱脸,都會(huì)用到對(duì)象序列化础米。Java序列化API為處理對(duì)象序列化提供了一個(gè)標(biāo)準(zhǔn)機(jī)制,該API簡(jiǎn)單易用添诉。
如何對(duì)Java對(duì)象進(jìn)行序列化與反序列化
在Java中屁桑,只要一個(gè)類實(shí)現(xiàn)了java.io.Serializable接口,那么它就可以被序列化栏赴。這里先來(lái)一段代碼:
代碼1創(chuàng)建一個(gè)用戶類蘑斧,用于序列化及反序列化
package com.hollis;
import java.io.Serializable;
import java.util.Date;
/**
* Created by hollis on 16/2/2.
*/
public class User implements Serializable{
private String name;
private int age;
private Date birthday;
private transient String gender;
private static final long serialVersionUID = -6849794470754667710L;
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 Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", gender=" + gender +
", birthday=" + birthday +
'}';
}
}
代碼2對(duì)用戶進(jìn)行序列化及反序列化的演示
package com.hollis;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.io.*;
import java.util.Date;
/**
* Created by hollis on 16/2/2.
*/
public class SerializableDemo {
public static void main(String[] args) {
//Initializes The Object
User user = new User();
user.setName("hollis");
user.setGender("male");
user.setAge(23);
user.setBirthday(new Date());
System.out.println(user);
//Write Obj to File
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(oos);
}
//Read Obj from File
File file = new File("tempFile");
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(file));
User newUser = (User) ois.readObject();
System.out.println(newUser);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(ois);
try {
FileUtils.forceDelete(file);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//output
//User{name='hollis', age=23, gender=male, birthday=Tue Feb 02 17:37:38 CST 2016}
//User{name='hollis', age=23, gender=null, birthday=Tue Feb 02 17:37:38 CST 2016}
序列化及反序列化相關(guān)知識(shí)
1,在Java中须眷,只要一個(gè)類實(shí)現(xiàn)了java.io.Serializable接口竖瘾,那么它就可以被序列化。
2柒爸,通過(guò)ObjectOutputStream和ObjectInputStream對(duì)對(duì)象進(jìn)行序列化及反序列化
3准浴,虛擬機(jī)是否允許反序列化,其他連續(xù)類路徑和功能代碼是否一致捎稚,一個(gè)非常重要的一點(diǎn)是兩個(gè)類的序列化ID是否一致(就是private static final long serialVersionUID)
4乐横,序列化并不保存靜態(tài)變量。
5今野,要想將父類對(duì)象也序列化葡公,就需要讓父類也實(shí)現(xiàn)Serializable接口。
6条霜,瞬態(tài)關(guān)鍵字的作用是控制變量的序列化催什,在變量聲明前加上該關(guān)鍵字,可以阻止該變量被序列化到文件中宰睡,在被反序列化后蒲凶,瞬態(tài)變量的值被設(shè)置為初始值,如int型的是0拆内,對(duì)象型的是null旋圆。
7,服務(wù)器端給客戶端發(fā)送序列化對(duì)象數(shù)據(jù)麸恍,對(duì)象中有一些數(shù)據(jù)是敏感的灵巧,某些密碼字符串等,希望該密碼分區(qū)在序列化時(shí),進(jìn)行加密,而客戶端如果擁有解密的密鑰玄柠,僅在客戶端進(jìn)行反序列化時(shí),才可以對(duì)密碼進(jìn)行讀取敏弃,這樣可以一定程度保證序列化對(duì)象的數(shù)據(jù)安全。
ArrayList的序列化
在介紹ArrayList序列化之前噪馏,先來(lái)考慮一個(gè)問(wèn)題:
如何自定義的序列化和反序列化策略
帶著這個(gè)問(wèn)題麦到,我們來(lái)看java.util.ArrayList的源碼
代碼3
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
transient Object[] elementData; // non-private to simplify nested class access
private int size;
}
筆者省略了其他成員變量虹茶,從上面的代碼中可以知道ArrayList實(shí)現(xiàn)了java.io.Serializable接口,那么我們就可以對(duì)它進(jìn)行序列化及反序列化隅要。因?yàn)閑lementData是transient的蝴罪,所以我們認(rèn)為這個(gè)成員變量不會(huì)被序列化而保留下來(lái)。我們寫一個(gè)Demo步清,驗(yàn)證一下我們的想法:
代碼4
public static void main(String[] args) throws IOException, ClassNotFoundException {
List<String> stringList = new ArrayList<String>();
stringList.add("hello");
stringList.add("world");
stringList.add("hollis");
stringList.add("chuang");
System.out.println("init StringList" + stringList);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("stringlist"));
objectOutputStream.writeObject(stringList);
IOUtils.close(objectOutputStream);
File file = new File("stringlist");
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
List<String> newStringList = (List<String>)objectInputStream.readObject();
IOUtils.close(objectInputStream);
if(file.exists()){
file.delete();
}
System.out.println("new StringList" + newStringList);
}
//init StringList[hello, world, hollis, chuang]
//new StringList[hello, world, hollis, chuang]
那么該數(shù)組實(shí)際上elementData就是用來(lái)保存列表中的元素的要门。通過(guò)該屬性的聲明方式我們知道,他是無(wú)法通過(guò)序列化持久化下來(lái)的廓啊。那么為什么code 4的結(jié)果卻通過(guò)序列化和反序列化把List中的元素保留下來(lái)了呢欢搜?
writeObject和readObject方法
在ArrayList中定義了來(lái)個(gè)方法:writeObject和readObject。
這里先提出論文:
在序列化過(guò)程中谴轮,如果被序列化的類中定義了writeObject和readObject方法炒瘟,虛擬機(jī)會(huì)引發(fā)調(diào)用對(duì)象類里的writeObject和readObject方法,進(jìn)行用戶自定義的序列化和反序列化第步。
如果沒(méi)有這樣的方法疮装,則默認(rèn)調(diào)用的是ObjectOutputStream的defaultWriteObject方法以及ObjectInputStream的defaultReadObject方法。
用戶自定義的writeObject和readObject方法可以允許用戶控制序列化的過(guò)程粘都,例如可以在序列化的過(guò)程中動(dòng)態(tài)改變序列化的數(shù)值廓推。
來(lái)看一下這兩個(gè)方法的具體實(shí)現(xiàn):
代碼5
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
代碼6
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
那么為什么ArrayList要用這種方式來(lái)實(shí)現(xiàn)序列化呢?
為什么瞬態(tài)
ArrayList實(shí)際上是動(dòng)態(tài)片段翩隧,每次在放滿以后自動(dòng)增長(zhǎng)設(shè)置的長(zhǎng)度值樊展,如果數(shù)組自動(dòng)增長(zhǎng)長(zhǎng)度設(shè)置為100,而實(shí)際只放了一個(gè)元素堆生,則會(huì)序列化99個(gè)null元素专缠。為了保證在序列化的時(shí)候不會(huì)將這么多null同時(shí)進(jìn)行序列化,ArrayList把元素?cái)?shù)組設(shè)置為瞬態(tài)淑仆。
為什么writeObject和readObject
但是涝婉,作為一個(gè)集合,在序列化過(guò)程中還必須保證其中的元素可以被持久化下來(lái)糯景。前面說(shuō)過(guò)嘁圈,為了防止一個(gè)包含大量空物體的序列被序列化省骂,以便優(yōu)化存儲(chǔ)蟀淮,所以,ArrayList使用transient來(lái)聲明elementData钞澳。 怠惶,所以,通過(guò)重新writeObject和readObject方法的方式把其中的元素保留下來(lái)轧粟。
writeObject方法把上面elementData的元素遍歷的保存到輸出流(ObjectOutputStream)中策治。
readObject方法從輸入流(ObjectInputStream)中識(shí)別對(duì)象并保存賦值到數(shù)組elementData中脓魏。
至此,我們先試著來(lái)回答剛剛提出的問(wèn)題:
如何自定義的序列化和反序列化策略
答:可以通過(guò)在被序列化的類中增加writeObject和readObject方法通惫。那么問(wèn)題又來(lái)了:
雖然ArrayList中寫了writeObject和readObject方法茂翔,但是這兩個(gè)方法并沒(méi)有顯示的被調(diào)用啊。
那么如果一個(gè)類中包含writeObject和readObject方法履腋,那么這兩個(gè)方法是怎么被調(diào)用的呢珊燎?
ObjectOutputStream
從代碼4中,我們可以研磨遵湖,對(duì)象的序列化過(guò)程通過(guò)ObjectOutputStream和ObjectInputputStream來(lái)實(shí)現(xiàn)的悔政,那么帶著剛剛的問(wèn)題,我們來(lái)分析一下ArrayList中的writeObject和readObject方法到底是如何被調(diào)用的呢延旧?
為了節(jié)省篇幅谋国,此處稱為ObjectOutputStream的writeObject的調(diào)用棧:
writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject
這里看一下invokeWriteObject:
void invokeWriteObject(Object obj, ObjectOutputStream out)
throws IOException, UnsupportedOperationException
{
if (writeObjectMethod != null) {
try {
writeObjectMethod.invoke(obj, new Object[]{ out });
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof IOException) {
throw (IOException) th;
} else {
throwMiscException(th);
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}
其中writeObjectMethod.invoke(obj, new Object[]{ out });是關(guān)鍵,通過(guò)反射的方式調(diào)用writeObjectMethod方法迁沫。官方是這么解釋這個(gè)writeObjectMethod的:
類定義的writeObject方法芦瘾;如果沒(méi)有,則為null
在我們的示例中集畅,這個(gè)方法就是我們?cè)贏rrayList中定義的writeObject方法旅急。通過(guò)反射的方式被調(diào)用了。
至此牡整,我們先試著來(lái)回答剛剛提出的問(wèn)題:
如果一個(gè)類中包含writeObject和readObject方法藐吮,那么這兩個(gè)方法是怎么被調(diào)用的?
答:在使用ObjectOutputStream的writeObject方法和ObjectInputStream的readObject方法時(shí)逃贝,會(huì)通過(guò)反射的方式調(diào)用谣辞。
至此,我們已經(jīng)介紹完了ArrayList的序列化方式沐扳。那么泥从,不知道有沒(méi)有人提出這樣的疑問(wèn):
Serializable明明就是一個(gè)空的接口,它是怎么保證只能實(shí)現(xiàn)該接口的方法才能進(jìn)行序列化與反序列化的呢沪摄?
Serializable接口的定義:
public interface Serializable {
}
讀者可以嘗試把代碼1中的繼承Serializable的代碼去掉躯嫉,再執(zhí)行代碼2,會(huì)拋出java.io.NotSerializableException杨拐。
其實(shí)這個(gè)問(wèn)題也很好回答祈餐,我們?cè)倩氐絼倓侽bjectOutputStream的writeObject的調(diào)用棧:
writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject
writeObject0方法中有這么一段代碼:
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
在進(jìn)行序列化操作時(shí),會(huì)判斷要被序列化的類是否為Enum哄陶,Array和Serializable類型帆阳,如果不是則直接拋出NotSerializableException。
總結(jié)
1屋吨,如果一個(gè)類想被序列化蜒谤,需要實(shí)現(xiàn)Serializable接口山宾。否則將引發(fā)NotSerializableException異常,這是因?yàn)轺⒒眨谛蛄谢僮鬟^(guò)程中序列類型進(jìn)行檢查资锰,要求被序列化的類必須屬于Enum,Array和Serializable類型其中的任何一種阶祭。
2台妆,在變量聲明前加上該關(guān)鍵字,可以阻止該變量被序列化到文件中胖翰。
3接剩,在類中增加writeObject和readObject方法可以實(shí)現(xiàn)自定義序列化策略