preface

In Java, Serializable is the most convenient method of serialization, but it is so cheap to use that it can be used without any understanding of how it works.

There are many scenarios that require serialization, and when it comes to efficiently transferring data from one place to another, serialization can be used. Focus on the goal of different, the implementation of Serializable is different, Serializable as a high rate of serialization means, naturally different from other serialization. This paper also mainly explains its own understanding of Serializable. Through this paper, it can be learned that:

  • What is object serialization
  • How is Serializable implemented
  • How is Serializable stored
  • What are the techniques for using Serializable

If you don’t know the answer to any of these questions, this article may be helpful.

What is object serialization

In simple terms, object serialization is the translation of object information at runtime, according to certain rules, into a series of tractable binary streams, and then transferring that binary stream from one side to the other. On the other hand, after receiving the binary stream, the receiver can deserialize and parse the object information to get useful data information according to the agreed parsing rules.

Object information, including Class information, inheritance relationship, access permission, variable type, and value information. Thus, the serialized binary stream contains not only the key information describing the Class of an object, but also the practical numeric information. Thus, serialization can be described as — relaying object information and storing object data.

How is Serializable implemented

In this case, how does Serializable implement serialization?

For use, we only need to declare the implementation of the Serializable interface. If you ask me how Serializable is implemented, most people can blurt it out and use reflection. If you ask a little more, why use reflection implementations, many people will be dumb.

Before we get into the real source code, let’s assume that if you were to write Serializable, how would you do it?

The objects that developers write vary widely and do not provide much information about the use of Serializable. However, the Class information of each object can be described by a corresponding Class type. The Class type contains the methods, member attributes, scope, access permission, inheritance relationship, and so on. Each type is a subclass of Object, and Object data stored in HEEP can be accessed at run time through the Object.

In this case, when you have an object to serialize, you can use reflection to extract the information. If an object is treated as the root node, then all the information about an object can be roughly represented by the following lesson tree:

In the figure above, the Class node can be decomposed further down until it is of type Object.

So the question that remains is how to use reflection to traverse the tree, to extract and store the key information.

ObjectStreamClass

ObjectStreamClass is used to describe the existence of an object in Serializable. In ObjectStreamClass, we don’t care about the contained methods except for the few methods that Serializable provides for this object type that can be implemented for other purposes. Because for the object that will be serialized, you care about its data structure, and the underlying property values of each property.

ObjectStreamClass takes Class types as parameters to instantiate and extracts information for each layer

private ObjectStreamClass(final Class<? > cl) {// represents the class type this.cl = cl; // Class name = cl.getName(); IsProxy = proxy.isProxyClass (cl); / / whether the Enum isEnum = Enum. Class. IsAssignableFrom (cl); / / whether implements serializable serializable = serializable. Class. IsAssignableFrom (cl); / / whether realized externalizable externalizable = externalizable. Class. IsAssignableFrom (cl); // The superclass type Class<? > superCl = cl.getSuperclass(); // superDesc is an ObjectStreamClass that represents the superclass superDesc = (superCl! = null) ? lookup(superCl, false) : null; ObjectStreamClass localDesc = this; if (serializable) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { if (isEnum) { // serialVersionUID suid = Long.valueOf(0); // Enum No attribute. Fields = NO_FIELDS; return null; } if (cl.isarray ()) {// fields = NO_FIELDS; return null; } // Get serialVersionUID suid = getDeclaredSUID(cl); Field = getSerialFields(cl); field = getSerialFields(cl); computeFieldOffsets(); } catch (InvalidClassException e) { serializeEx = deserializeEx = new ExceptionInfo(e.classname, e.getMessage()); fields = NO_FIELDS; } the if (externalizable) {/ / implements Enternalizable reflected for constructor cons = getExternalizableConstructor (cl); } else {/ / reflection for constructor cons = getSerializableConstructor (cl); 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); }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} ReadResolveMethod = getInheritableMethod(cl, "readResolve", null, object.class); return null; }}); } else { suid = Long.valueOf(0); fields = NO_FIELDS; }... }Copy the code

The ObjectStreamClass instantiation process can see the key points:

  • Enums and collection types do not collect FIELDS
  • A customized serialVersionUID is obtained. If no, it is generated
  • Member properties are represented as ObjectStreamField and obtained by getSerialFields(). In this method, GetDeclaredSerialFields () attempts to get the serialized member properties declared by the serialPersistentFields variable. If not, the member properties declared in the type are obtained through the getDefaultSerialFields() reflection

Take a look at getDefaultSerialFields ()

private static ObjectStreamField[] getDefaultSerialFields(Class<? > cl) {// get all fields Field[] clFields = cl.getdeclaredFields (); ArrayList<ObjectStreamField> list = new ArrayList<>(); / / static and transient int mask mask = Modifier. The static | Modifier. The transient; for (int i = 0; i < clFields.length; I ++) {if ((clFields[I].getModifiers() &mask) == 0) {// Add all fields that are not declared STATIC or TRANSIENT, Add (new ObjectStreamField(clFields[I], false, true)); } } int size = list.size(); return (size == 0) ? NO_FIELDS : list.toArray(new ObjectStreamField[size]); }Copy the code

Other kinds of information are also acquired primarily through reflection. Some of the methods seen in the source above:

  • writeObject()
  • readObject()
  • readObjectNoData()
  • writeReplace()
  • readResolve()

Serializable provides access to methods that can be customizable, as described below.

ObjectStreamField

ObjectStreamClass is used to describe an object that has properties. ObjectStreamField describes the property information.

The structure of ObjectStreamField is relatively simple. It only needs to describe the properties of an object

ObjectStreamField(Field Field, Boolean unshared, Boolean showType) {// Reflect the class representing the property, Field this. Field = Field; this.unshared = unshared; // Attribute name = field.getName(); Class<? Class<? Class<? Class<? Class<? Class<? > ftype = field.getType(); / / this attribute types, is the basic type, or object type type = (showType | | ftype. IsPrimitive ())? ftype : Object.class; // Get the class identifier signature = getClassSignature(ftype).intern(); }Copy the code

Signature records the class identifier. A class identifier is a string representing a type, which in the current context is:

  • I: Integer
  • B: Byte
  • J: Long
  • F: Float
  • D: Double
  • S: Short
  • C: Character
  • Z: Boolean
  • V: Void
  • L… : Reference type

L… If a class is com.qinx.Example, the class identifier is L/com/qinx/Example.

HandleTable

HandleTable is Serializable cache pool. When the same information is serialized or deserialized, if there is cache information in the cache pool, it can reduce many unnecessary parsing. For serialization columns, the following information is cached:

  • Class
  • ObjectStreamClass
  • String
  • Array
  • Enum

HandleTable source code is not deep, only understand the role can also.

In fact, with ObjectStreamClass, ObjectStreamField, HandleTable, and the idea of extracting information from an object mentioned above, the Serializable implementation can guess the pattern. Because you know how to get information, how to represent objects and properties, and how to reduce parsing in the process. The next section is the source process.

ObjectOutputStream

The entry class for serialization is ObjectOutputStream

Public ObjectOutputStream(OutputStream out) throws IOException {// Verify the permission verifySubclass(); // Construct an OutputStream of type BlockDataOutputStream. Bout = New BlockDataOutputStream(out); // Bout = New BlockDataOutputStream(out); // Cache handles = new HandleTable(10, (float) 3.00); Subs = new ReplaceTable(10, (float) 3.00); WriteObjectOverride () enableOverride = false; writeObjectOverride() = false; // Write the magic number and version number to the bout writeStreamHeader(); // Set the mode of BlockDataOutputStream. If true, data is written in bout bout. SetBlockDataMode (true); . }Copy the code

The entry method for serialization is writeObject()

Public final void writeObject(Object obj) throws IOException {if (enableOverride) { That is, when you customize ObjectOutputStream and use a no-parameter construct, writeObjectOverride() needs to be overridden to do the serialization. writeObjectOverride(obj); return; } try { writeObject0(obj, false); } catch (IOException ex) {if (depth == 0) {// Note that writeFatalException(ex); } throw ex; }}Copy the code

In general, ObjectOutputStream() takes a parameter construct and writeObject0() is used for serialization. Remember the idea of extracting object information, the variable depth, which you see here, is the depth of the node currently traversing the tree.

The following source code is a bit long, patience to see

boolean oldMode = bout.setBlockDataMode(false); depth++; try { // handle previously written and non-replaceable objects int h; Subs.lookup () returns obj itself if (obj = subs.lookup(obj)) == null) {subs.lookup() returns obj itself if obj does not need to be replaced. Null writeNull(); return; } else if (! unshared && (h = handles.lookup(obj)) ! = -1) {// If obj has already been parsed, write the index to it. return; } else if (obj instanceof Class) {// write lass((Class) obj, unshared); return; } else if (obj instanceof ObjectStreamClass) {// How ObjectStreamClass is treated, 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; ObjectStreamClass desc = objectStreamClass.lookup (cl, true); ObjectStreamClass desc = objectStreamClass.lookup (cl, true); if (! desc.hasWriteReplaceMethod() || (obj = desc.invokeWriteReplace(obj)) == null || (repCl = obj.getClass()) == cl) { // // 1. This type does not implement writeReplace() // 2. This type implements the writeReplace() replacement object, but returns an empty object // 3. This type implements writeReplace() and returns an empty object, getting the type break of the object being replaced; } cl = repCl; } if (enableReplace) {// replaceObject() returns yes by default, ObjectOutputStream subclass can override replaceObject() to operate on some objects Object rep = replaceObject(obj); if (rep ! = obj && rep ! = null) {// If obj is replaced with a different type of object, parse that type of information with ObjectOutputStream cl = rep.getClass(); desc = ObjectStreamClass.lookup(cl, true); } // obj = rep; } // If (obj! Subs. assign(orig, obj); If (obj == null) {// null case writeNull(); return; } else if (! unshared && (h = handles.lookup(obj)) ! = -1) {// writeHandle(h); return; } else if (obj instanceof Class) {// Class type writeClass((Class) obj, unshared); return; } else if (obj instanceof ObjectStreamClass) {// ObjectStreamClass type writeClassDesc((ObjectStreamClass) obj, unshared); return; 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 {// restore the traversal depth; bout.setBlockDataMode(oldMode); }Copy the code

In summary, the following things have been done:

  1. It first checks and processes if obj is replaced, parsed, is of type Class, and ObjectStreamClass. In this case, information is sent to the bout through their respective processing methods. The handling of Class and ObjectStreamClass is not the focus of this article and therefore will not be extended. Processing cases where OBj has been replaced and parsed are simpler to write and less to post routine code
  2. Objectstreamclass.lookup () ¶ Obj is not processed and its type description is obtained by objectStreamClass.lookup (). If ObjectOutputStream overrides the replacement method, then obj is replaced and the object is replaced with its description, because the operation is based on the actual obj to be written
  3. When a substitution occurs, override execution 1
  4. When obj is a String, a collection, an enumeration, and an object type, the first three are not difficult and do not extend. The focus is on how the object type is handled. Along the same lines, eventually when a member variable of a type no longer contains an object type, no further resolution is required (the parent case is treated as the object type case).
  5. Restore traversal depth

So, the point of concern is objectStreamClass.lookup () if you get the object description, and how writeOrdinaryObject() handles the parsing of the object type.

Gets the object description ObjectStreamClass

static ObjectStreamClass lookup(Class<? > cl, boolean all) { if (! (all | | Serializable. Class. IsAssignableFrom (cl))) {/ / if all is true, all types can get object description / / otherwise, Only types that implement Serializable can get object description return null; } ObjectStreamClass is associated with a Class and can be unloaded by the JVM, so when classes are unloaded, The description of the object associated with the Class is also invalid // To remove this relationship and re-establish the relationship processQueue(Caches. LocalDescsQueue, Caches. LocalDescs); Class WeakClassKey key = new WeakClassKey(CL, Caches.localdescsqueue); // WeakClassKey = new WeakClassKey(CL, Caches.localdescsqueue); // Get EntryFutrue or ObjectStreamClass // Key is actually Class hashCode Reference<? > ref = Caches.localDescs.get(key); Object entry = null; if (ref ! = null) {// Get the actual reference type entry = ref.get(); } EntryFuture future = null; If (entry == null) {// Entry is recycled // create EntryFuture, use soft reference to associate EntryFuture newEntry = new EntryFuture(); Reference<? > newRef = new SoftReference<>(newEntry); do { if (ref ! = null) { Caches.localDescs.remove(key, ref); } / / when parsing the type for the first time, or the entry is recycling, must get entry, / / which will be the key that is associated with EntryFuture placeholder ref = Caches. LocalDescs. PutIfAbsent (key, newRef); if (ref ! = null) { entry = ref.get(); } // If (ref! = ref! = ref! = null && entry == null); if (entry == null) { future = newEntry; }} if (entry instanceof ObjectStreamClass) {return (ObjectStreamClass) entry; } if (entry instanceof EntryFuture) {future = (EntryFuture) entry; If (Future.getowner () == thread.currentThread ()) {if (Future.getowner () == thread.currentThread ()); } else {// Entry = future.get();} entry = future.get(); }} if (entry == null) {try {// Create object description entry = new ObjectStreamClass(cl); } catch (Throwable th) { entry = th; } if (future. Set (entry)) {// indicate that object descriptions created by the current thread can be used // associate with Class Caches.localdescs.put (key, new SoftReference<Object>(entry)); } else {// specify that the object description for the current thread thread is not available, and that another thread has already created it // Use the object description created by another thread entry = Future.get (); }}... }Copy the code

The ultimate goal of the lookup() method is to get a usable description of the object:

  • In the initial phase, if all is false, the type is checked to see if Serializable is implemented. If Serializable is implemented, the next step can be taken
  • If you can get an object description, you need to check whether it is possible, because the class may have been uninstalled, the description is invalid, and you need to rebuild it
  • ObjectStreamClass(final Class
    cl) to get the available object description. But the reality is a little more complicated, because at this point in time, there are likely to be multiple threads serializing the same type, so concurrency needs to be considered
  • When dealing with concurrency issues, the EntryFuture is used as a placeholder for the Class association, where the current thread is stored. The EntryFuture is then retrieved from the cache again. The EntryFuture is available. At this point, although the current thread used the EntryFuture created by itself to occupy the space, it does not mean that the EntryFuture can be successfully occupied, so the EntryFuture obtained from the cache again is the real object that has succeeded in occupying the space. Then get the object description entry from the EntryFuture. Note that the current phase ensures that all threads get the same EntryFuture, but the entry inside does not necessarily exist yet. So in this case, the object description will be tried to create an EntryFuture.(Set) association. On association, the description is available; Entryfuture.get () ¶ EntryFuture.get() ¶

Writing object Information

private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) throws IOException { ...... Try {// check whether obj can serialize desc.checkSerialize(); // Write information. The value of TC_OBJECT is 0x73, indicating that a new object is to be written. Bout. // Write object description writeClassDesc(desc, false); Handles. Assign (unshared?) null : obj); if (desc.isExternalizable() && ! Desc.isproxy ()) {// Implement Externalizable() case writeExternalData((Externalizable) obj); } else {// writeSerialData(obj, desc); // writeSerialData(obj, desc); } } finally { if (extendedDebugInfo) { debugInfoStack.pop(); }}}Copy the code

Object information consists of two aspects, type information and data information, both of which are stored in the serialized binary stream. Type information is written using writeClassDesc(), which contains rules for writing in four cases:

  • Description is null
  • Describes when it is cached
  • Describes the type when a dynamic proxy is generated
  • When the description has not been written

Instead of going deeper into writeClassDesc(), go deeper into the actual binary stream writing rules.

Data information is written via 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; The if (slotDesc hasWriteObjectMethod ()) {/ / there is mentioned in front, the object has an operational approach entrance, writeObject () / / if the object implements this method, it will reflect with writeObject ()... } else {// In general, execute this method defaultWriteFields(obj, slotDesc); }}}Copy the code

The comments are clear, so go to defaultWriteFields()

private void defaultWriteFields(Object obj, ObjectStreamClass desc) throws IOException { Class<? > cl = desc.forClass(); // Make sure that the object description obtained from Obj is correct if (cl! = null && obj ! = null && ! cl.isInstance(obj)) { throw new ClassCastException(); }... ObjectStreamField[] fields = desc.getFields(false); Object[] objVals = new Object[descal.getNuMobjFields ()]; Int numPrimFields = fields. Length - objVals. Length; GetObjFieldValues (obj, objVals); // Reflect to get the attribute value desc.getobjFieldValues (obj, objVals); for (int i = 0; i < objVals.length; i++) { if (extendedDebugInfo) { debugInfoStack.push( "field (class \"" + desc.getName() + "\", name: \"" + fields[numPrimFields + i].getName() + "\", type: \"" + fields[numPrimFields + i].getType() + "\")"); } try {// writeObject0(objVals[I], fields[numPrimFields + I].isunshared ()); } finally { if (extendedDebugInfo) { debugInfoStack.pop(); }}}}Copy the code

The object description contains information about all member variables of a given type. Once you have this information, you iterate through the call to writeObject0() to parse the member variable. It is important to note here that only the object type is resolved by traversal, whereas the basic type can be determined directly. In desc.getNumObjFields() above, you get the number of class member variables of object type, which are computed by computeFieldOffsets() after the attributes are resolved during the object description instantiation phase.

Once you get back to writeObject0(), you get familiar.

Summary of Serializable implementation

  1. The object is treated as a tree, and the type, parent class, and member variables are treated as nodes, traversing the tree to obtain information
  2. By default, through ObjectOutputStream. WriteObject0 (), use the system to provide analytical information way; You can also override writeObjectOverride() to parse according to your own rules
  3. The description of object and attribute are resolved by reflection, and the description of member variable is stored in the description of object
  4. HandleTable is used to cache parsing information, including Class, ObjectStreamClass, String, Array, Enum
  5. If ObjectOutputStream overrides replaceObject(), some objects can be replaced during serialization
  6. Write the Object description information and the member information of the basic type, and resolve them into the member attributes of the Object type through 2-6 process, until a class is Object or all member variables are basic types, and the tree converges.

storage

The final solution of serialization is to get a regular set of binary streams. This part of written information is distributed in the margins and corners of the serialization process code. If you want to post the relevant code of this part of information, it will inevitably lose its focus and no nutrition. So this section will not see the actual code being written.

If you’ve ever been familiar with the Class file structure, this part is easy to digest and doesn’t hurt. Parsing Class files

Simply put, the serialized binary stream needs to contain the type description of the serialized object, the parent class, the property description, the property value information, and the ultimate goal is to be transferred to deserialize to form valid data. Therefore, the binary stream is compact, does not carry redundant information, and should be safe, not missing any information. In addition to the necessary and inevitable information such as description, parent class, attribute description, attribute value, both serialization and deserialization must follow the rules of reading the binary stream, plus some mnemonic, index information, and finally form an index table carrying information.

Write an example

public class Phone implements Serializable { private Card card1; private Card card2; private String brand; private String color; private int price; . } public class Card implements Serializable { private String number; . } public static void main(String[] args) { String FILE_PATH = "info.txt"; try { File f = new File(FILE_PATH); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(FILE_PATH)); Phone phone = new Phone(); phone.setColor("0xFFFFFF"); phone.setBrand("OnePlus"); phone.setPrice(3000); Card card1 = new Card("12345678901"); Card card2 = new Card("98765432109"); phone.setCard1(card1); phone.setCard2(card2); out.writeObject(phone); out.flush(); out.close(); } catch (IOException e) { e.printStackTrace(); }}Copy the code

The above code phone serialized, write to the file, and then borrow the binary tool to open the file, there is the following information

It converts binary data into hexadecimal, and each position is a bytecode. If you don’t have tools, you can use ByteArrayOutputSteam to receive streams, and then with DadatypeConverter you can output hexadecimal content in the console as well.

Add a reading rule and you can read the above information. Below are the reading rules I read as I look at the source code.

The meaning of the picture is:

  • Colored squares represent one type of block
  • The entire structure behind the color represents the structure that the color block might have
  • The number represents how many bytes

The reading rules given above do not represent all the rules. If you are interested in more rules, see Serializable Specification

If your example is different, follow the diagram. The following is a detailed reading explanation of the binaries serialized out of the current example. Many of these constants can be found in ObjectStreamConstants. If you need to write your own rules, it helps to index the source code with ObjectStreamConstants. Attach the HandleTable table for the current situation

Each line below contains sixteen bytecodes

AC ED(Magic) 00 05(version) 73(New object) 72(New Object Description) 00 05(class name length) 50 68 6F 6E 65(representing ascil code for Phone) 79 52 E9 A2 1A 63 D9 5C(serialVersionUID of Phone) 02(Serializable implemented) 00 05(number of attributes 5) 49(attribute type I) 00 05(length of attribute name 5) 70 72 69 63 65(Ascil code of Price) 4C(attribute type L) 00 05(attribute name length 5) 62 72 61 6E 64(representing brand's AScil code) 74(new String) 00 12(class name face length 18) 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B(Ljava/lang/String; Ascil code) 4C(attribute type L) 00 05(attribute name length 5) 63 61 72 64 31(representing card1 ascil code) 74(new String, Will point to the literal of the following attribute type) 00 06(class name face length 6) 4C 43 61 72 64 3B(LCard; Ascil code) 4C(attribute type L) 00 05(attribute name length 5) 63 61 72 64 32(representing card2's ascil code) 71(written marker) 00 7E00 02(here 0x7E0000 + 0x02, the former represents the cache index marker, The latter is the position, because it's written as an Int, which is four bytes, So we have 00) 4C(attribute type L) 00 05(attribute name length 5) 63 6F 6C 6F 72(color ascil code) 71(written marker) 00 7E 00 01(first position in cache table) 78(object end marker) 70(empty object) 00 00 0B B8(int, 3000,price) 74(new String) 00 07(literal length 7) 4F 6E 65 50 6C 75 73(OnePlus ascil code) 73(new object) 72(New object description) 00 04(class name) 43 61 72 64(Card ascil code) A0 66 51 7E C3 D5 7D 4A(Card serialVersionUID) 02(meaning Serializable implemented) 00 01(attribute number 1) 4C(attribute type L) 00 06(attribute name length 6) 6E 75 6D 62 65 72(number's ascil code) 71(written marker) 00 7E 00 01(first position of HandleTable) 78(end of object) 70(object is empty) 74(new String) 00 0B(literal length 11) 31 32 33 34 35 36 37 38 39 30 31 73(1245678901 ascil code) 71(written mark) 00 7E 00 05(5th position of HandleTable) 74(new String) 00 0B(value literal length 11) 39 38 37 36 35 34 33 32 31 30 39(98765432109 ACILL code) 74(new String) 00 08(value literal length 8) 30 78 46 46 46 46 46 46(0xFFFFFF ascil code)Copy the code

Please point out any mistakes.

Use skills

As mentioned earlier, the Serializable mechanism provides several entry points. On the object side, there is

  • writeObject()
  • readObject()
  • readObjectNoData()
  • writeReplace()
  • readResolve()

And serialPersistentFields

In analytic measurement, there are:

  • writeObjectOverride()
  • replaceObject()

Here are some examples of these operations

serialPersistentFields

Objects can implement serialPersistentFields that declare serialization rules, for example, in the Phone code above

    private static final ObjectStreamField[] serialPersistentFields = {
            new ObjectStreamField("brand", String.class),
            new ObjectStreamField("card1", Card.class),
    };
Copy the code

After the serialization, only the information of brand and card1 is included, and the information of other attributes is not entered.

SerialPersistentFields declares which properties to serialize, and transient declares which properties to not serialize, so no validation is done here. Specific code about serialPersistentFields, visible ObjectSteamClass getSerialFields () – > ObjectSteamClass. GetDeclaredSerialFields ()

writeObject

Objects can declare writeObject(), which can be manipulated before the property is serialized. You can add code to your Phone

Private void writeObject (Java. IO. ObjectOutputStream steam) throws Java IO. IOException {/ / let's mobile phone is on sale price = price * 7 / 10; // Continue to use the default serialization process steam.defaultwriteObject (); }Copy the code

In the original code, the price was set to 3000, which was discounted by 30% before serialization, and changed to 2100, which corresponds to 0x00000834 in hexadecimal. In a real scenario, this can be used for specific data encryption operations. Since there is writeObject() to preprocess, there is readObject() to preprocess. This process is reflected in deserialization, so I won’t do the example here.

See ObjectOutputStream writeObject code. WriteOrdinaryObject () – > writeSerialData ()

writeReplace

Objects can implement writeReplace(), which can be replaced before the object description is obtained. The code is located in the ObjectOutputStream. WriteObject0 (). And in that code, you see two layers of information:

  1. This is where you can modify the value of a property in an object
  2. You can replace an entire object

In the first case, add code to the Phone

private Object writeReplace(){ Card replaceCard = new Card("super card!!!" ); card1 = replaceCard; return this; }Copy the code

Replace card1 with another card, and here is the serialized binary stream

Card1 has been replaced

Let’s change the code that we just did and look at the second case

    private Object writeReplace(){
        return "all phone sale out";
    }
Copy the code

Replace Phone with String, and here is the binary stream

Phone has been replaced entirely. As you can see here, the default String does not come with a serialVersionUID.

readResolve

ReadResolve (), like writeReplace(), can also replace an object. ReadResolve () is a process that resolves an object during deserialization, then operates on it and returns a new object. I’m not going to do an example here.

readObjectNoData

ReadObjectNoData () can be implemented during serialization if all the properties of the object have no value. It is possible to do something in this scenario, such as setting a special value.

private void readObjectNoData(){
}
Copy the code

replaceObject()

You can customize ObjectOutputStream() to implement replaceObject(), deserialize, and replace objects such as MyObjectOutputStream, and use the ObjectOutputStream in the example of such a replacement

public class MyObjectOutputStream extends ObjectOutputStream { public MyObjectOutputStream(OutputStream out) throws IOException { super(out); enableReplaceObject(true); } @Override protected Object replaceObject(Object obj) throws IOException { if (obj instanceof Card){ Card curCard = (Card)obj; curCard.setNumber("857857857"); } return super.replaceObject(obj); }}Copy the code

The serialized information is

The replacement is complete. Code is located in the ObjectOutputStream. WriteObject0 ()

writeObjectOverride

WriteObjectOverride () can also be implemented to define the serialization process if the system-provided serialization process is not satisfied. Replace the MyObjectOutputStream code with:

public class MyObjectOutputStream extends ObjectOutputStream { public MyObjectOutputStream(OutputStream out) throws IOException { super(out); // enableOverride is private, in order to reflect changes here Class<? > parentCl = getClass().getSuperclass(); try { Field enableOverride = parentCl.getDeclaredField("enableOverride"); enableOverride.setAccessible(true); enableOverride.set(this, true); } catch (Exception e) { e.printStackTrace(); }} @override protected void writeObjectOverride(Object obj) throws IOException {// writeBytes("I know the Serializable!!" ); }}Copy the code

Complete serialization, the code is located in the ObjectOutputStream. WriteObject ().

conclusion

I’ll end with writeObjectOverride(), and good luck knowing the Serializable when you’re done. Deserialization is not covered in this article, but after you know the principle of deserialization, deserialization can be seen, want to understand the principle of deserialization will be easy, so do not do a statement. There is also no example to verify the use of serialVersionUID. If you are interested, you can verify yourself.

answer

1. What is object serialization the type, attribute, parent class and data information of an object are parsed into binary according to certain rules, stored and transmitted, and meaningful objects can be obtained through deserialization.

2, Serializable how to implement the object as the root node, the type, parent class, member variables as child nodes, traversing the tree, reflection to obtain information. And record the process information with ObjectSteamClass, ObjectStreamField, and HandleTable. Finally, the information is output to a binary stream by convention.

3. How Serializable is stored in binary streams. You can think of this stream as an index table carrying data

4, Serializable has some tips for using object side:

  • WriteObject (): Manipulates object data before serialization, such as encryption
  • ReadObject (): Manipulates object data before deserialization, such as unmasking
  • ReadObjectNoData () : Provides special handling when an object has no property data
  • WriteReplace () : Replaces the serialized object
  • ReadResolve () : Replaces the deserialized object

Analytical side:

  • WriteObjectOverride () : Custom serialization process
  • ReplaceObject () : The aspect replaces the object to be serialized

reference

Android Serializable objects that you don’t know about

Java Serializable principle analysis