Java序列化浅析

定义

Java序列化是指将Java对象保存为二进制字节码的过程;Java反序列化表示将二级制字节码转化为Java对象的过程。

Java序列化的原因

  1. 由于Java对象的生命周期比JVM短,JVM停止运行之后,Java对象就不复存在,如果想要在JVM停止运行之后,获取Java对象,就需要对其进行序列化后进行保存。
  2. 需要将Java对象通过网络传输时(rmi),通过将Java对象序列化为二进制的形式在网络中传输。

Java序列化的原理

  1. 可以通过ObjectInputStreamObjectOutputStreamreadObject() writeObject() 方法进行反序列化和序列化。

    实现代码

    待序列化对象

     @Data
     @ToString
     public class LearnDTO implements Serializable {
         private String name;
         private int age;
         private String nation;
     }

    序列化方法

    public class SerializableLearning {
    
        public static void main(String[] args) {
            LearnDTO dto = new LearnDTO();
            dto.setName("gavin");
            dto.setAge(18);
            dto.setNation("汉");
            System.out.println("序列化对象----开始");
            writeObject(dto);
            System.out.println("序列化对象----结束");
    
            System.out.println("反序列化对象----开始");
            readObject();
            System.out.println("反序列化对象----结束");
        }
    
        public static void writeObject(LearnDTO dto) {
            try (FileOutputStream file = new FileOutputStream(new File("test"));
                 ObjectOutputStream os = new ObjectOutputStream(file)) {
                os.writeObject(dto);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        public static void readObject() {
            try (FileInputStream file = new FileInputStream(new File("test"));
                 ObjectInputStream oi = new ObjectInputStream(file)) {
                LearnDTO o = (LearnDTO) oi.readObject();
                System.out.println(o.toString());
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

    运行结果

        序列化对象----开始
        序列化对象----结束
        反序列化对象----开始
        LearnDTO(name=gavin, age=18, nation=汉)
        反序列化对象----结束
  2. 序列化的对象必须实现 java.io.Serializable接口。

    ObjectOutputStream在实现序列化的方法中,限制了序列化对象必须是StringArrayEnum以及实现了Serializable接口的类型。因此自定义的对象要进行序列化必须实现java.io.Serializable接口。

    源代码

    // 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());
        }
    }
  3. 在序列化对象时,如果有一些变量的值不想被记录下来,可以通过static(静态变量)或者 transient(瞬态变量)关键词修饰变量。注意:static变量 反序列化的值为 类中被赋予的初始值。

    源代码

    Java序列化中通过ObjectStreamClass保存序列化对象的信息,在通过对象class初始化ObjectStreamClass对象时,通过getDefaultSerialFields方法保存要序列化的字段,此时会检查字段是否被statictransient关键词修饰。

    private static ObjectStreamField[] getDefaultSerialFields(Class cl) {
        Field[] clFields = cl.getDeclaredFields();
        ArrayList list = new ArrayList<>();
        // Modifier.STATIC | Modifier.TRANSIENT
        int mask = Modifier.STATIC | Modifier.TRANSIENT;
    
        for (int i = 0; i < clFields.length; i++) {
            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]);
    }
  4. 在待序列化的对象中通常要指定serialVersionUID值。否则当你改动对象的任何字段后,改动前就已经保存的序列化对象无法进行反序列化,在反序列化时将抛出java.io.InvalidClassException异常。

    源代码

    readObject() 反序列化方法内,通过读取序列化文件的数据获取 sid的值和序列化对象的sid的值进行一致性判断。

    if (model.isEnum != osc.isEnum) {
        throw new InvalidClassException(model.isEnum ?
                "cannot bind enum descriptor to a non-enum class" :
                "cannot bind non-enum descriptor to an enum class");
    }
    
    if (model.serializable == osc.serializable &&
            !cl.isArray() &&
            suid != osc.getSerialVersionUID()) {
        throw new InvalidClassException(osc.name,
                "local class incompatible: " +
                        "stream classdesc serialVersionUID = " + suid +
                        ", local class serialVersionUID = " +
                        osc.getSerialVersionUID());
    }
  5. 通过实现java.io.Externalizable接口,然后重写writeExternal()readExternal()方法,可以自定义序列化以及反序列化的方式。Externalizable接口继承了Serializable接口。在ObjectInputStreamObjectOutputStream进行序列化和反序列化时会判断是否实现此接口,从而决定是否调用重写的writeExternal()readExternal()方法。

    源代码

    ObjectOutputStream.writeOrdinaryObject方法中判断是否实现Externalizable接口。并在writeExternalData方法中调用重写的writeExternal方法。

    private void writeOrdinaryObject(Object obj,
                                     ObjectStreamClass desc,
                                     boolean unshared)
        throws IOException
    {
               ...
        if (desc.isExternalizable() && !desc.isProxy()) {
            writeExternalData((Externalizable) obj);
        } else {
            writeSerialData(obj, desc);
        }
               ...
    }

Java序列化的缺点

  1. Java自身的序列化功能,必须和Java对象类型绑定。如果反序列化的项目没有对应的Java类型,则在反序列化时就会抛出ClassNotFoundException异常。这大大限制了Java序列化的使用场景。
  2. Java序列化的数据流通过byte[]数组传输,生成的字节数组流太大不仅占用内存空间,而且不利于进行网络传输。

针对Java序列化的缺点,项目中很少使用Java序列化的功能,在设计对象序列化时通常采用第三方的序列化框架,常用的序列化工具有:转JSON类工具、Hessian、Kryo、Xstream、Protobuf等。

具体的序列化工具分析可以参考一下文章:

https://juejin.im/post/6844903918879637518#heading-5

你可能感兴趣的