Java的序列化和反序列化機制 問題導入: 在閱讀ArrayList源碼的時候,註意到,其內部的成員變數動態數組elementData被Java中的關鍵字transient修飾 transient關鍵字意味著Java在序列化時會跳過該欄位(不序列化該欄位) 而Java在預設情況下會序列化類(實現了J ...
Java的序列化和反序列化機制
問題導入:
在閱讀ArrayList
源碼的時候,註意到,其內部的成員變數動態數組elementData
被Java中的關鍵字transient
修飾
transient
關鍵字意味著Java在序列化時會跳過該欄位(不序列化該欄位)
而Java在預設情況下會序列化類(實現了Java.io.Serializable
介面的類)的所有非瞬態(未被transient
關鍵字修飾)和非靜態('未被static關鍵字修飾')欄位
為什麼ArrayList
要給非常重要的動態數組成員變數elementData
添加transient
關鍵字?
事實上,ArrayList
給elementData
添加transient
關鍵字的原因是因為Java預設的序列化方法並不理想
- 空間效率: 由於擴容機制,
elementData
數組的容量可能會大於實際存儲的元素數量,數組中可能存在未使用的空間,如果直接走Java預設的序列化,直接序列化整個數組,會將這部分未使用的空間也一起序列化,導致空間浪費 - 控制序列化行為: 通過自定義
writeObject()
和readObject()
方法,ArrayList
能夠更好地控制序列化和反序列化過程,僅序列化實際包含的元素,併在反序列化時重新創建合適的數組大小
那麼,Java的序列化機制,標識介面Java.io.Serializable
和關鍵字transient
等是如何運作的?
從兩個類說起
Java中實現序列化和反序列化的兩個核心類是ObjectInputStream
和ObjectOutputStream
ObjectOutputStream
:將Java對象的原始數據類型以流的方式寫出到文件,實現對象的持久化存儲ObjectInputStream
:將文件中保存的對象,以流的方式取出來使用
一個簡單的示例
//1.創建一個類 實現序列化介面(標識該類可被序列化,如果不實現該介面,調用序列化方法會報java.io.NotSerializableException)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Serializable {
private String name;
private Integer age;
//標記remark欄位 不會被序列化
private transient String remark;
}
//2.序列化和反序列化演示
@Test
public void test(){
//創建對象
Person person = new Person();
person.setName("void");
person.setAge(26);
person.setRemark("hello world");
//指定 目標位置
String target = "F:\\out\\s.txt";
//序列化 演示
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(Files.newOutputStream(Paths.get(target)))) {
objectOutputStream.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
}
//反序列化 演示
try (ObjectInputStream objectInputStream = new ObjectInputStream(Files.newInputStream(Paths.get(target)))) {
Person person1 = (Person) objectInputStream.readObject();
log.info("person1:{}", person1);
//person1:Person(name=void, age=26, remark=null) 註意這裡的remark欄位,有transient關鍵字修飾和沒有是兩個結果
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
源碼解析
前文說到
Serializable
起標識作用,標識該類可被序列化,如果不實現該介面,調用序列化方法會報java.io.NotSerializableException
transient
關鍵字標記的欄位不會被序列化
從源碼來驗證:
Serializable起標識作用原理
java.io.ObjectOutputStream#writeObject0()
方法中的代碼片段
可以看到,如果這個類既不是字元串,數組,枚舉類,也沒有實現Serializable介面,就會報(NotSerializableException
)錯
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
...
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());
}
}
...
}
//...
transient關鍵字標記的欄位不會被序列化原理
java.io.ObjectStreamClass.getDefaultSerialFields
中的代碼片段
這裡涉及一種關鍵的數學和電腦科學知識點,即通過位運算,一個整數能夠被精確無誤地分解為多個具有唯一確定性的二進位子串。換言之,對於任何整數,我們都可以利用位運算技術將其分割成多個獨一無二、確定無疑的二進位表示狀態
private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
Field[] clFields = cl.getDeclaredFields();
ArrayList<ObjectStreamField> list = new ArrayList<>();
//註意點1: Modifier 是 Java中用來表示修飾符的一個類 一個整數可以通過位運算聚合多種狀態
int mask = Modifier.STATIC | Modifier.TRANSIENT;
for (int i = 0; i < clFields.length; i++) {
//註意點2: 通過位運算與(都是1才是1),判斷如果該欄位 既不是static修飾也不是transient修飾的欄位 就需要序列化
if ((clFields[i].getModifiers() & mask) == 0) {
list.add(new ObjectStreamField(clFields[i], false, true));
}
}
int size = list.size();
return (size == 0) ? NO_FIELDS :
list.toArray(new ObjectStreamField[size]);
}
怎麼自定義序列化和反序列化方法?
參考ArrayList
源碼
//ArrayList中的自定義序列化方法
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
int expectedModCount = modCount;
//註意點1:調用 ObjectOutputStream的預設 序列化方法將該序列化的欄位序列化
s.defaultWriteObject();
//註意點2:額外寫入數組的實際裝了多少元素(不是總容量)
//Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
//註意點3:依次寫入數組元素
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
//註意點4:調用ObjectInputStream的預設 反序列化方法將該反序列化的欄位反序列化
s.defaultReadObject();
//註意點5:這裡讀取的值是被忽略的
// Read in capacity
s.readInt(); // ignored
//註意點6: 依次反序列化
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, 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();
}
}
}
參考源碼註釋和補充的批註能大概理解整個流程,但是這裡有個地方比較讓我疑惑
結合註意點2,和註意點5發現ArrayList
在自定義序列化方法額外寫入了size
但是反序列化時僅僅只做了讀取並沒有使用,源碼註釋也是//ignore
,序列化寫入的時候也提了一下寫入size
是為了相容clone()
行為
參考文章https://www.zhihu.com/question/359634731 應該是版本相容問題
新的問題?為什麼寫了writeObject()
方法和readObject()
方法,序列化和反序列化就會按照自定義的來?
序列化反序列化自定義原理
還是結合源碼分析
//1.以下為java.io.ObjectOutputStream#writeSerialData()的源碼
private void writeSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
//註意點1:這裡進行了是否有WriteObject方法的判定
if (slotDesc.hasWriteObjectMethod()) {
PutFieldImpl oldPut = curPut;
curPut = null;
SerialCallbackContext oldContext = curContext;
if (extendedDebugInfo) {
debugInfoStack.push(
"custom writeObject data (class \"" +
slotDesc.getName() + "\")");
}
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bout.setBlockDataMode(true);
slotDesc.invokeWriteObject(obj, this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
} finally {
curContext.setUsed();
curContext = oldContext;
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
curPut = oldPut;
} else {
defaultWriteFields(obj, slotDesc);
}
}
}
//2.進入方法 slotDesc.hasWriteObjectMethod()
boolean hasWriteObjectMethod() {
requireInitialized();
//註意點2:這裡對成員變數writeObjectMethod 進行了判斷 以此為依據來確定類是否含有writeObject方法 什麼時候賦值的?(初始化)
return (writeObjectMethod != null);
}
//3.在java.io.ObjectStreamClass.ObjectStreamClass(java.lang.Class<?>)類構造方法中 進行了初始化
private ObjectStreamClass(final Class<?> cl){
...
if(externalizable){
cons=getExternalizableConstructor(cl);
}else{
cons=getSerializableConstructor(cl);
//註意點3:這裡使用了反射機製為成員變數writeObjectMethod是否含有方法writeObject方法進行了賦值判定
writeObjectMethod=getPrivateMethod(cl,"writeObject",
new Class<?>[]{ObjectOutputStream.class },
Void.TYPE);
readObjectMethod=getPrivateMethod(cl,"readObject",
new Class<?>[]{ObjectInputStream.class },
Void.TYPE);
readObjectNoDataMethod=getPrivateMethod(
cl,"readObjectNoData",null,Void.TYPE);
hasWriteObjectData=(writeObjectMethod!=null);
}
...
}