Java序列化機(jī)制詳解
Serializable 接口
Serializable 接口是 Java 提供的標(biāo)記接口,沒有包含任何需要實(shí)現(xiàn)的方法。實(shí)現(xiàn)了這個接口的類表明其對象是可序列化的,可以被轉(zhuǎn)換為字節(jié)流。
public interface Serializable { }
通過實(shí)現(xiàn) Serializable 接口,標(biāo)識類的對象可以被序列化。這使得對象可以在網(wǎng)絡(luò)上傳輸或保存到文件中,而不失去其狀態(tài)和結(jié)構(gòu)。
序列化過程
序列化是將對象的狀態(tài)(字段值)轉(zhuǎn)換為字節(jié)流的過程。這個過程由 ObjectOutputStream 類來完成。序列化使得對象可以以字節(jié)流的形式進(jìn)行存儲或傳輸,便于在不同系統(tǒng)之間進(jìn)行數(shù)據(jù)交換。如下我們列舉幾個重要的方法的源碼:
writeObject
方法
public final void writeObject(Object obj) throws IOException { if (enableOverride) { writeObjectOverride(obj); return; } try { writeObject0(obj, false); } catch (IOException ex) { if (depth == 0) { writeFatalException(ex); } throw ex; } }
enableOverride 表示是否啟用了對象寫入的覆蓋機(jī)制。如果啟用,會調(diào)用 writeObjectOverride 方法來執(zhí)行對象的特定寫入邏輯。
如果沒有啟用覆蓋機(jī)制,則調(diào)用 writeObject0 方法執(zhí)行實(shí)際的對象序列化。
writeObject0 方法負(fù)責(zé)處理對象的序列化,其中第二個參數(shù) false 表示不使用不共享的方式進(jìn)行序列化。
如果在序列化過程中拋出 IOException 異常,會捕獲該異常。如果當(dāng)前深度為0(表示不在嵌套序列化過程中),則調(diào)用 writeFatalException 方法來處理異常,否則將異常重新拋出。
writeObject0
private void writeObject0(Object obj, boolean unshared) throws IOException { boolean oldMode = bout.setBlockDataMode(false); depth++; try { // handle previously written and non-replaceable objects int h; if ((obj = subs.lookup(obj)) == null) { writeNull(); return; } else if (!unshared && (h = handles.lookup(obj)) != -1) { writeHandle(h); return; } else if (obj instanceof Class) { writeClass((Class) obj, unshared); return; } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); return; } // check for replacement object Object orig = obj; Class<?> cl = obj.getClass(); ObjectStreamClass desc; for (;;) { // REMIND: skip this check for strings/arrays? Class<?> repCl; desc = ObjectStreamClass.lookup(cl, true); if (!desc.hasWriteReplaceMethod() || (obj = desc.invokeWriteReplace(obj)) == null || (repCl = obj.getClass()) == cl) { break; } cl = repCl; } if (enableReplace) { Object rep = replaceObject(obj); if (rep != obj && rep != null) { cl = rep.getClass(); desc = ObjectStreamClass.lookup(cl, true); } obj = rep; } // if object replaced, run through original checks a second time if (obj != orig) { subs.assign(orig, obj); if (obj == null) { writeNull(); return; } else if (!unshared && (h = handles.lookup(obj)) != -1) { writeHandle(h); return; } else if (obj instanceof Class) { writeClass((Class) obj, unshared); return; } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); return; } } // remaining cases 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()); } } } finally { depth--; bout.setBlockDataMode(oldMode); } }
反序列化過程
當(dāng)需要從字節(jié)流中恢復(fù)對象時(shí),Java 序列化機(jī)制會將字節(jié)流還原為對象的狀態(tài)。這個過程由 ObjectInputStream 類來完成。如下我們列舉幾個重要的方法的源碼:
readObject()
private final Object readObject(Class<?> type) throws IOException, ClassNotFoundException { if (enableOverride) { return readObjectOverride(); } if (! (type == Object.class || type == String.class)) throw new AssertionError("internal error"); // if nested read, passHandle contains handle of enclosing object int outerHandle = passHandle; try { Object obj = readObject0(type, false); handles.markDependency(outerHandle, passHandle); ClassNotFoundException ex = handles.lookupException(passHandle); if (ex != null) { throw ex; } if (depth == 0) { vlist.doCallbacks(); } return obj; } finally { passHandle = outerHandle; if (closed && depth == 0) { clear(); } } }
這段代碼的主要作用是根據(jù)給定的類型 (type) 進(jìn)行對象的反序列化。在這個過程中,它使用了一些狀態(tài)變量,如 enableOverride、passHandle、handles、depth、vlist 等,來管理反序列化的過程。在處理嵌套對象時(shí),它通過 markDependency 方法標(biāo)記了當(dāng)前對象與封閉對象的依賴關(guān)系。在深度為 0 時(shí),執(zhí)行了clear方法。具體反序列化執(zhí)行的核心方法是readObject0()
readObject0()
private Object readObject0(Class<?> type, boolean unshared) throws IOException { boolean oldMode = bin.getBlockDataMode(); if (oldMode) { int remain = bin.currentBlockRemaining(); if (remain > 0) { throw new OptionalDataException(remain); } else if (defaultDataEnd) { /* * Fix for 4360508: stream is currently at the end of a field * value block written via default serialization; since there * is no terminating TC_ENDBLOCKDATA tag, simulate * end-of-custom-data behavior explicitly. */ throw new OptionalDataException(true); } bin.setBlockDataMode(false); } byte tc; while ((tc = bin.peekByte()) == TC_RESET) { bin.readByte(); handleReset(); } depth++; totalObjectRefs++; try { switch (tc) { case TC_NULL: return readNull(); case TC_REFERENCE: // check the type of the existing object return type.cast(readHandle(unshared)); case TC_CLASS: if (type == String.class) { throw new ClassCastException("Cannot cast a class to java.lang.String"); } return readClass(unshared); case TC_CLASSDESC: case TC_PROXYCLASSDESC: if (type == String.class) { throw new ClassCastException("Cannot cast a class to java.lang.String"); } return readClassDesc(unshared); case TC_STRING: case TC_LONGSTRING: return checkResolve(readString(unshared)); case TC_ARRAY: if (type == String.class) { throw new ClassCastException("Cannot cast an array to java.lang.String"); } return checkResolve(readArray(unshared)); case TC_ENUM: if (type == String.class) { throw new ClassCastException("Cannot cast an enum to java.lang.String"); } return checkResolve(readEnum(unshared)); case TC_OBJECT: if (type == String.class) { throw new ClassCastException("Cannot cast an object to java.lang.String"); } return checkResolve(readOrdinaryObject(unshared)); case TC_EXCEPTION: if (type == String.class) { throw new ClassCastException("Cannot cast an exception to java.lang.String"); } IOException ex = readFatalException(); throw new WriteAbortedException("writing aborted", ex); case TC_BLOCKDATA: case TC_BLOCKDATALONG: if (oldMode) { bin.setBlockDataMode(true); bin.peek(); // force header read throw new OptionalDataException( bin.currentBlockRemaining()); } else { throw new StreamCorruptedException( "unexpected block data"); } case TC_ENDBLOCKDATA: if (oldMode) { throw new OptionalDataException(true); } else { throw new StreamCorruptedException( "unexpected end of block data"); } default: throw new StreamCorruptedException( String.format("invalid type code: %02X", tc)); } } finally { depth--; bin.setBlockDataMode(oldMode); } }
serialVersionUID
serialVersionUID 是用于版本控制的序列化版本號。它是一個長整型數(shù)值,用于標(biāo)識類的版本。通過顯式聲明 serialVersionUID,可以在類結(jié)構(gòu)發(fā)生變化時(shí)依然能夠正確地進(jìn)行反序列化。
@Data public class LoginUserInfo implements Serializable { private static final long serialVersionUID = 1L; ... }
如果在類中沒有明確聲明 serialVersionUID,Java 運(yùn)行時(shí)系統(tǒng)會根據(jù)類的結(jié)構(gòu)自動生成一個。這種自動生成的 serialVersionUID 是基于類的各個方面的,包括字段、方法、父類等。如果類的結(jié)構(gòu)發(fā)生變化,可能導(dǎo)致自動生成的 serialVersionUID 發(fā)生變化。這可能會導(dǎo)致在反序列化時(shí),類的版本不一致,從而導(dǎo)致 InvalidClassException 異常。
所以顯式聲明 serialVersionUID是確保反序列化過程正確的關(guān)鍵,避免因類結(jié)構(gòu)變化而導(dǎo)致的問題。
transient 關(guān)鍵字
關(guān)鍵字 transient 用于標(biāo)記字段,表示在對象序列化的過程中,這個字段應(yīng)該被忽略。例如,如果一個類有一個不希望被序列化的緩存字段,可以使用 transient 關(guān)鍵字來避免將其寫入序列化數(shù)據(jù)。例如ArrayList、LinkedList 等類中的一些屬性就是使用transient修飾的:
_20231218215928.jpg
_20231218215951.jpg
自定義序列化和反序列化
有時(shí)候,可能需要自定義序列化和反序列化的過程以滿足特定需求??梢酝ㄟ^實(shí)現(xiàn) writeObject 和 readObject 方法來實(shí)現(xiàn)自定義邏輯。如ArrayList類中就是通過自定義的序列化和反序列化方法:
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(); } } /** * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is, * deserialize it). */ 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 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(); } } }
序列化不保存靜態(tài)變量
- 對象狀態(tài) vs. 類狀態(tài)
序列化的主要目的是保存對象的狀態(tài),即對象的實(shí)例變量。靜態(tài)變量是類級別的,它們對于每個對象實(shí)例都是相同的。序列化關(guān)注的是對象的實(shí)例狀態(tài),因?yàn)檫@是對象在不同環(huán)境中重建時(shí)所需的關(guān)鍵信息。
- 節(jié)省空間
靜態(tài)變量通常用于表示類級別的常量或共享數(shù)據(jù),這些數(shù)據(jù)在所有對象實(shí)例之間是相同的。如果每個對象的靜態(tài)變量都被序列化并存儲,將導(dǎo)致冗余,浪費(fèi)存儲空間。序列化的目標(biāo)之一是盡可能緊湊地保存對象的狀態(tài),因此不保存靜態(tài)變量是一種優(yōu)化。
- 不需要還原
靜態(tài)變量在類加載時(shí)初始化,并在整個應(yīng)用程序的生命周期內(nèi)保持不變。因此,在反序列化時(shí)不需要重新初始化靜態(tài)變量。序列化和反序列化的目標(biāo)是保存和還原對象的動態(tài)狀態(tài),而不是類級別的靜態(tài)狀態(tài)。
序列化的安全性和性能考慮
在實(shí)際應(yīng)用中,需要注意序列化的安全性和性能。反序列化過程中可能存在安全風(fēng)險(xiǎn),因此要謹(jǐn)慎處理來自不受信任源的序列化數(shù)據(jù)。此外,對于大量數(shù)據(jù)的序列化,可能會影響系統(tǒng)性能,可以考慮使用更高效的序列化工具或壓縮算法。
總結(jié)
綜合來看,Java 序列化的核心思想是將對象的狀態(tài)轉(zhuǎn)換為字節(jié)流,并通過 ObjectOutputStream 類完成這一過程。該類在內(nèi)部處理了對象引用的記錄、對象字段的寫入、自定義寫入方法的執(zhí)行等。在實(shí)際應(yīng)用中,我們需要注意序列化版本控制、對象字段的 transient 關(guān)鍵字的處理以及序列化性能等方面的問題。
請注意,Java 序列化機(jī)制在現(xiàn)代應(yīng)用中可能會遇到一些挑戰(zhàn),包括性能問題、安全性問題以及與其他語言的兼容性等。因此,在一些場景下,開發(fā)者可能會考慮使用其他序列化框架,如 JSON 或 Protocol Buffers,以滿足不同的需求。
以上就是Java序列化機(jī)制詳解的詳細(xì)內(nèi)容,更多關(guān)于Java序列化機(jī)制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot利用自定義注解實(shí)現(xiàn)隱私數(shù)據(jù)脫敏(加密顯示)的解決方案
這兩天在整改等保測出的問題,里面有一個“用戶信息泄露”的風(fēng)險(xiǎn)項(xiàng)(就是后臺系統(tǒng)里用戶的一些隱私數(shù)據(jù)直接明文顯示了),其實(shí)指的就是要做數(shù)據(jù)脫敏,本文給大家介紹了SpringBoot利用自定義注解實(shí)現(xiàn)隱私數(shù)據(jù)脫敏(加密顯示)的解決方案,需要的朋友可以參考下2023-11-11java反射實(shí)現(xiàn)javabean轉(zhuǎn)json實(shí)例代碼
基于java反射機(jī)制實(shí)現(xiàn)javabean轉(zhuǎn)json字符串實(shí)例,大家參考使用吧2013-12-12Mybatis-plus支持Gbase8s分頁的實(shí)現(xiàn)示例
本文主要介紹了Mybatis-plus支持Gbase8s分頁的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11Spring Security使用權(quán)限注解實(shí)現(xiàn)精確控制
在現(xiàn)代的應(yīng)用系統(tǒng)中,權(quán)限管理是確保系統(tǒng)安全性的重要環(huán)節(jié),Spring Security作為Java世界最為普及的安全框架,提供了強(qiáng)大而靈活的權(quán)限控制功能,這篇文章將深入探討Spring Security使用權(quán)限注解實(shí)現(xiàn)精確控制,需要的朋友可以參考下2024-12-12Spring?Framework六種常見設(shè)計(jì)模式
設(shè)計(jì)模式是軟件開發(fā)的重要組成部分,本文借助spring來講解這個框架的設(shè)計(jì)模式,通過本文我們探討了spring如何利用這些模式來提供這些豐富的功能,對本文感興趣的朋友跟隨小編一起看看吧2023-06-06java中不同版本JSONObject區(qū)別小結(jié)
本文主要介紹了java中不同版本JSONObject區(qū)別小結(jié),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-02-02java實(shí)現(xiàn)人工智能化屏幕監(jiān)控窗口
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)人工智能化屏幕監(jiān)控窗口,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-09-09