• Java 序列化


    1、Java对象序列化

    JDK1.1 中引入的一组开创性特性之一,用于作为一种将 Java 对象的状态转换为字节数组,以便存储或传输的机制,以后,仍可以将字节数组转换回 Java 对象原有的状态
    1.1、基本点
    对象序列化保存的是对象的"状态",即它的成员变量。由此可知,对象序列化不会关注类中的“静态变量”;
    在 Java 中,只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化;实现 Externalizable,自己要对序列化内容进行控制,控制哪些属性可以被序列化,哪些不能被序列化
    通过 ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化;
    虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致,就是 private static final long serialVersionUID;
    transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null;
    Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的 5 字节的存储空间就是新增引用和一些控制信息的空间.反序列化时,恢复引用关系;该存储规则极大的节省了存储空间;

    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
    Test test = new Test();
    test.i = 1;
    out.writeObject(test);
    out.flush();
    test.i = 2;
    out.writeObject(test);
    out.close();
    ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
         "result.obj"));
    Test t1 = (Test) oin.readObject();
    Test t2 = (Test) oin.readObject();
    System.out.println(t1.i);// 1
    System.out.println(t2.i);// 1
    // 结果两个输出的都是 1, 原因就是第一次写入对象以后,第二次再试图写的时候,虚拟机根据引用关系
    // 知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用,所以读取时,都是第一次保存的对象

    1.2、子类与父类序列化

    • 要想将父类对象也序列化,就需要让父类也实现Serializable接口;
    • 如果父类实现了Serializable接口,子类但没有实现Serializable接口,子类拥有一切可序列化相关的特性,子类可以序列化;
    • 如果子类实现Serializable接口,父类不实现,根据父类序列化规则,父类的字段数据将不被序列化,从而达到部分序列化的功能;
    • 在反序列化时仍会调用父类的构造器,只能调用父类的无参构造函数作为默认的父对象。如果父类没有默认构造方法则在反序列化时会出异常.
    • 如果父类实现了 Serializable 接口,要让子类不可序列化,可以在子类中写如下代码:(其实违反了里氏替换原则)
    private void writeObject(java.io.ObjectOutputStream out) throws IOException{
     throw new NotSerializableException("不可写");
    }
    private void readObject(java.io.ObjectInputStream in) throws IOException{
     throw new NotSerializableException("不可读");
    }
    • 序列化与反序列化时子类和父类构造方法调用关系:序列化时子类递归调用父类的构造函数,反序列化作用于子类对象时如果其父类没有实现序列化接口则其父类的默认无参构造函数会被调用。如果父类实现了序列化接口则不会调用构造方法

    2、如何序列化

    在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject方法,虚拟机会试图调用对象类里的writeObject和readObject方法,进行用户自定义的序列化和反序列化。如果没有这样的方法,则默认调用是ObjectOutputStream的defaultWriteObject方法以及ObjectInputStream的defaultReadObject方法。用户自定义的writeObject和readObject方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值;
    2.1、ArrayList序列化实现
    ArrayList使用上述实现:为什么ArrayList要用这种方式来实现序列化呢?
    为什么 transient Object[] elementData?:
    ArrayList实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为100,而实际只放了一个元素,那就会序列化99个null元素.为了保证在序列化的时候不会将这么多null同时进行序列化,ArrayList 把元素数组设置为transient
    为什么要写方法:writeObject and readObject
    前面提到为了防止一个包含大量空对象的数组被序列化,为了优化存储,所以,ArrayList 使用 transient 来声明elementData作为一个集合,在序列化过程中还必须保证其中的元素可以被持久化下来,所以,通过重写writeObject 和 readObject方法的方式把其中的元素保留下来.writeObject方法把elementData数组中的元素遍历的保存到输出流(ObjectOutputStream)中。readObject方法从输入流(ObjectInputStream)中读出对象并保存赋值到elementData数组中
    2.2、自定义序列化和反序列化策略
    可以通过在被序列化的类中增加writeObject和readObject方法。那么问题又来了;

    • 那么如果一个类中包含writeObject和readObject方法,那么这两个方法是怎么被调用的呢?在使用ObjectOutputStream的writeObject方法和ObjectInputStream的readObject方法时,会通过反射的方式调用
      ①、ObjectOutputStream 的writeObject的调用栈:writeObject —> writeObject0 —>writeOrdinaryObject—>writeSerialData—>invokeWriteObject
      ②、这里看一下invokeWriteObject:其中writeObjectMethod.invoke(obj, new Object[]{ out });是关键,通过反射的方式调用writeObjectMethod方法
      2.3、Serializable如何实现序列化与反序列化
      Serializable 明明就是一个空的接口,它是怎么保证只有实现了该接口的方法才能进行序列化与反序列化的呢?看ObjectOutputStream 的writeObject的调用栈:
      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() + "
    " + debugInfoStack.toString());
     } else {
      throw new NotSerializableException(cl.getName());
     }
    }

    在进行序列化操作时,会判断要被序列化的类是否是Enum、Array和Serializable类型,如果不是则直接抛出NotSerializableException
    2.4、writeReplace()和readResolve()
    Serializable除提供了writeObject和readObject标记方法外还提供了另外两个标记方法可以实现序列化对象的替换(即 writeReplace 和 readResolve)
    2.4.1、writeReplace:序列化类一旦实现了 writeReplace 方法后则在序列化时就会先调用 writeReplace 方法将当前对象替换成另一个对象,该方法会返回替换后的对象.接着系统将再次调用另一个对象的 writeReplace方法,直到该方法不再返回另一个对象为止,程序最后将调用该对象的writeObject() 方法来保存该对象的状态

    • 实现了 writeReplace 的序列化类就不要再实现 writeObject 了,因为该类的 writeObject
      方法就不会被调用;
    • 实现 writeReplace 的返回对象必须是可序列化的对象;
    • 通过 writeReplace 序列化替换的对象在反序列化中无论实现哪个方法都是无法恢复原对象的。
    • 所以 writeObject 只和 readObject 配合使用,一旦实现了 writeReplace 在写入时进行替换就不再需要writeObject 和 readObject 了。

    2.4.2、readResolve:方法可以实现保护性复制整个对象,会紧挨着序列化类实现的 readObject() 之后被调用,该方法的返回值会代替原来反序列化的对象而原来序列化类中 readObject()反序列化的对象将会立即丢弃.readObject()方法在序列化单例类时尤其有用,单例序列化都应该提供 readResolve() 方法,这样才可以保证反序列化的对象依然正常。

    3、serialVersionUID

    private static final long serialVersionUID:每个可序列化类相关联
    该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类;
    如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException;
    为保证 serialVersionUID 值跨不同 java 编译器实现的一致性,序列化类必须声明一个明确的 serialVersionUID ;
    使用 private 修饰符显示声明 serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类 – serialVersionUID 字段作为继承成员没有用处;
    类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的serialVersionUID,也有可能相同
    显式地定义serialVersionUID有两种用途:
    ①.在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID
    ②.当你序列化了一个类实例后,希望更改一个字段或添加一个字段,不设置serialVersionUID,所做的任何更改都将导致无法反序化旧有实例,并在反序列化时抛出一个异常。如果你添加了serialVersionUID,在反序列旧有实例时,新添加或更改的字段值将设为初始化值(对象为null,基本类型为相应的初始默认值),字段被删除将不设置

    4、反序列化

    • 实现 Serializable 接口的对象在反序列化时不需要调用对象所在类的构造方法,完全基于字节,如果是子类继承父类的序列化,那么将调用父类的构造方法;
    • 实现 Externalizable 接口的对象在反序列化时会调用构造方法.该接口继承自 Serializable,使用该接口后基于 Serializable 接口的序列化机制就会失效,因为:
    1.Externalizable 不会主动序列化,当使用该接口时序列化的细节需要由我们自己去实现.
    2.使用 Externalizable 主动进行序列化时当读取对象时会调用被序列化类的无参构方法去创建一个
    新的对象,然后再将被保存对象的字段值分别填充到新对象中。
    3.所以 所以实现 Externalizable 接口的类必须提供一个无参 public 的构造方法,
    readExternal 方法必须按照与 writeExternal 方法写入值时相同的顺序和类型来读取属性值。 

    5、序列化实现对象的拷贝

    内存中通过字节流的拷贝是比较容易实现的.把母对象写入到一个字节流中,再从字节流中将其读出来,这样就可以创建一个新的对象了,并且该新对象与母对象之间并不存在引用共享的问题,真正实现对象的深拷贝

    public class CloneUtils {
     @SuppressWarnings("unchecked")
     public static <T extends Serializable> T clone(T   obj){
      T cloneObj = null;
      try {
       //写入字节流
       ByteArrayOutputStream out = new ByteArrayOutputStream();
       ObjectOutputStream obs = new ObjectOutputStream(out);
       obs.writeObject(obj);
       obs.close();
       //分配内存,写入原始对象,生成新对象
       ByteArrayInputStream ios = new  ByteArrayInputStream(out.toByteArray());
       ObjectInputStream ois = new ObjectInputStream(ios);
       //返回生成的新对象
       cloneObj = (T) ois.readObject();
       ois.close();
      } catch (Exception e) {
       e.printStackTrace();
      }
      return cloneObj;
     }
    }
     

    6、常见的序列化协议

    6.1、COM:主要用于windows 平台,并没有实现跨平台,其序列化原理是利用编译器中的虚表
    6.2、CORBA:早期比较好的实现了跨平台,跨语言的序列化协议,COBRA 的主要问题是参与方过多带来的版本过多,版本之间兼容性较差,以及使用复杂晦涩;
    6.3、XML&SOAPXML 是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点;注意xml中一些特殊字符的处理;SOAP(Simple Object Access protocol)是一种被广泛应用的,基于XML为序列化和反序列化协议的结构化消息传递协议;SOAP具有安全、可扩展、跨语言、跨平台并支持多种传输层协议
    6.4、JSON(Javascript Object Notation)

    • ①、这种Associative array格式非常符合工程师对对象的理解;
    • ②、它保持了XML的人眼可读(Human-readable)的优点;
    • ③、相对xml而言,序列化都的数据更简洁;
    • ④、它具备Javascript的先天性支持,所以被广泛应用于Web browser的应用常景中,是Ajax的事实标准协议;
    • ⑤、与XML相比,其协议比较简单,解析速度比较快;
    • ⑥、松散的Associative array使得其具有良好的可扩展性和兼容性
      6.5、Thrift:是 Facebook 开源提供的一个高性能,轻量级 RPC 服务框架,其产生正是为了满足当前大数据量、分布式、跨语言、跨平台数据通讯的需;其并不仅仅是序列化协议,而是一个 RPC 框架;由于Thrift的序列化被嵌入到Thrift框架里面,Thrift框架本身并没有透出序列化和反序列化接口,这导致其很难和其他传输层协议共同使用;
      6.6、Protobuf:
    • ①.标准的IDL和IDL编译器,这使得其对工程师非常友好;
    • ②.序列化数据非常简洁,紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10;
    • ③.解析速度非常快,比对应的XML快约20-100倍;
    • ④.提供了非常友好的动态库,使用非常简介,反序列化只需要一行代码;

    7、JSON 序列化

    7.1、关于Map转json输出顺序问题

    Map<String, String> map = new LinkedHashMap<String, String>();
    map.put("b""2");
    map.put("a""1");
    map.put("c""3");
    System.out.println(JSON.toJSON(map));// {"a":"1","b":"2","c":"3"}
    Map<String, String> map1 = new LinkedHashMap<String, String>();
    map1.put("b""2");
    map1.put("a""1");
    map1.put("c""3");
    Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create();
    System.out.println(gson.toJson(map1)); // {"b":"2","a":"1","c":"3"}
    • 使用fastjson或者jdk自带的序列化,默认是无序输出的,如果需要使用fastJson输出有序的json:JSONObject
    // 构造的时候使用 new JSONObject(true):
    JSONObject object = new JSONObject(true);
    Map<String, String> map2 = new LinkedHashMap<String, String>();
    map2.put("b""2");
    map2.put("a""1");
    map2.put("c""3");
    object.putAll(map2);
    System.out.println(JSONObject.toJSON(object));// {"b":"2","a":"1","c":"3"}
    • Gson 保证了你插入的顺序,就是正常的Map迭代操作

    8、序列化安全

    • 序列化在传输中是不安全的:因为序列化二进制格式完全编写在文档中且完全可逆,所以只需将二进制序列化流的内容转储到控制台就可以看清类及其包含的内容,故序列化对象中的任何private 字段几乎都是以明文的方式出现在序列化流中。可能面临信息泄露、数据篡改、拒绝服务等
    • 要解决序列化安全问题的核心原理就是避免在序列化中传递敏感数据,所以可以使用关键字 transient 修饰敏感数据的变量。或者通过自定义序列化相关流程对数据进行签名加密机制再存储或者传输
      1.对序列化的流数据进行加密;
      2.在传输的过程中使用TLS加密传输;
      3.对序列化数据进行完整性校验;
      4.针对信息泄露:使用transient标记敏感字段;
      5.针对数据篡改:实现ObjectInputValidation接口并重写其方法;
      6.针对整个对象伪造:通过重写ObjectInputStream的resolveClass来实现

    9、Java默认序列化与二进制编码

    • 字节码流大小
    • 序列化耗时
    别废话,拿你代码给我看。
  • 相关阅读:
    我有好多东西要学
    不科学计数法
    遍历生成dataframe
    搭建个人博客,Docsify+Github webhook+JGit解决方案
    从1开始实现一个中间件
    python通过youget下载B站系列视频
    JS 中深拷贝的几种实现方法
    addEventListener() 方法,事件监听
    css背景透明文字不透明
    如何理解dispatch( 'tagsView/delAllViews',null,{root:true} )里面的root:true
  • 原文地址:https://www.cnblogs.com/lvxueyang/p/13707538.html
Copyright © 2020-2023  润新知