美文网首页
Serializable

Serializable

作者: 凯玲之恋 | 来源:发表于2020-08-20 11:22 被阅读0次
    20200630204142.png

    是 Java 提供的序列化接口,它是一个空接口:

    Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。

    Serializable 入门



    Serializable 有以下几个特点:

    • 可序列化类中,未实现 Serializable 的属性状态无法被序列化/反序列化
    • 也就是说,反序列化一个类的过程中,它的非可序列化的属性将会调用无参构造函数重新创建
    • 因此这个属性的无参构造函数必须可以访问,否者运行时会报错
    • 一个实现序列化的类,它的子类也是可序列化的

    序列化与反序列化 Serializable

    Serializable 的序列化与反序列化分别通过 ObjectOutputStream 和 ObjectInputStream 进行



    Java 的序列化步骤与数据结构分析

    序列化算法一般会按步骤做如下事情:

    • 将对象实例相关的类元数据输出。
    • 递归地输出类的超类描述直到不再有超类。
    • 类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
    • 从上至下递归输出实例的数据

    格式化后以二进制打开


    • AC ED: STREAM_MAGIC. 声明使用了序列化协议.
    • 00 05: STREAM_VERSION. 序列化协议版本.
    • 0x73: TC_OBJECT. 声明这是一个新的对象.
    • 0x72: TC_CLASSDESC. 声明这里开始一个新 Class。
    • 00 2e: Class 名字的长度.

    readObject/writeObject 原理分析

    writeObject

    1. ObjectOutputStream 的构造函数设置 enableOverride = false
      以oos.writeObject(obj)为例分析


    2. 所以 writeObject 方法执行的是 writeObject0(obj, false);


    3. 在 writeObject0 方法中,代码非常多,看重点


    4. 在 writeOrdinaryObject(obj, desc, unshared)方法中


    5. writeSerialData 方法,主要执行方法:defaultWriteFields(obj, slotDesc)

    1. 在 ObjectStreamClass 中,ObjectOutputStream(ObjectInputStream)会寻找目标类中的私有的 writeObject(readObject)方法,赋值给变量 writeObjectMethod(readObjectMethod)


    Serializable序列化

    • 序列化(serialize) - 序列化是将对象转换为字节流。
    • 反序列化(deserialize) - 反序列化是将字节流转换为对象。
    • 序列化用途
      • 序列化可以将对象的字节序列持久化——保存在内存、文件、数据库中。
      • 在网络上传送对象的字节序列。
      • RMI(远程方法调用)

    注意:使用 Java 对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量

    序列化和反序列化

    Java 通过对象输入输出流来实现序列化和反序列化:

    • java.io.ObjectOutputStream 类的 writeObject() 方法可以实现序列化;
    • java.io.ObjectInputStream 类的 readObject()方法用于实现反序列化。

    序列化和反序列化示例:

    // Gender类,表示性别
    // 每个枚举类型都会默认继承类java.lang.Enum,而Enum类实现了Serializable接口,所以枚举类型对象都是默认可以被序列化的。
    public enum Gender {  
        MALE, FEMALE  
    } 
    
    // Person 类实现了 Serializable 接口,它包含三个字段。另外,它还重写了该类的 toString() 方法,以方便打印 Person 实例中的内容。
    public class Person implements Serializable {  
        private String name = null;  
        private Integer age = null;  
        private Gender gender = null;  
    
        public Person() {  
            System.out.println("none-arg constructor");  
        }  
     
        public Person(String name, Integer age, Gender gender) {  
            System.out.println("arg constructor");  
            this.name = name;  
            this.age = age;  
            this.gender = gender;  
        }  
     
        // 省略 set get 方法
        @Override 
        public String toString() {  
            return "[" + name + ", " + age + ", " + gender + "]";  
        }  
    } 
     
    // SimpleSerial类,是一个简单的序列化程序,它先将Person对象保存到文件person.out中,然后再从该文件中读出被存储的Person对象,并打印该对象。
    public class SimpleSerial {  
        public static void main(String[] args) throws Exception {  
            File file = new File("person.out");  
            ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file)); // 注意这里使用的是 ObjectOutputStream 对象输出流封装其他的输出流
            Person person = new Person("John", 101, Gender.MALE);  
            oout.writeObject(person);  
            oout.close();  
     
            ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));  // 使用对象输入流读取序列化的对象
            Object newPerson = oin.readObject(); // 没有强制转换到Person类型  
            oin.close();  
            System.out.println(newPerson);  
        }  
    } 
     
    // 上述程序的输出的结果为:
    arg constructor  
    [John, 31, MALE]
    

    当重新读取被保存的Person对象时,并没有调用Person的任何构造器,看起来就像是直接使用字节将Person对象还原出来的。
    当Person对象被保存到person.out文件后,可以在其它地方去读取该文件以还原对象,但必须确保该读取程序的 CLASSPATH 中包含有 Person.class(哪怕在读取Person对象时并没有显示地使用Person类,如上例所示),否则会抛出 ClassNotFoundException。
    简单的来说,Java 对象序列化就是把对象写入到输出流中,用来存储或传输;反序列化就是从输入流中读取对象。
    序列化一个对象首先要创造某些OutputStream对象(如FileOutputStream、ByteArrayOutputStream等),然后将其封装在一个ObjectOutputStream对象中,在调用writeObject()方法即可序列化一个对象
    反序列化的过程需要创造InputStream对象(如FileInputstream、ByteArrayInputStream等),然后将其封装在ObjectInputStream中,在调用readObject()即可

    为什么一个类实现了Serializable接口,它就可以被序列化?

    被序列化的类必须属于EnumArraySerializable类型其中的任何一种,否则将抛出 NotSerializableException异常。这是因为:在序列化操作过程中会对类型进行检查,如果不满足序列化类型要求,就会抛出异常。

    使用ObjectOutputStream来持久化对象到文件中,使用了writeObject方法,该方法又调用了如下方法:

    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());  
            }  
        }  
        ...  
    }
    

    从上述代码可知,如果被写对象的类型是String,或数组,或Enum,或Serializable,那么就可以对该对象进行序列化,否则将抛出NotSerializableException。
    即、String类型的对象、枚举类型的对象、数组对象,都是默认可以被序列化的

    默认序列化机制

    如果仅仅让某个类实现Serializable接口,而没有其它任何处理的话,则就是使用默认序列化机制。

    使用默认机制在序列化对象时,不仅会序列化当前对象,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化,以此类推。

    所以,如果一个对象包含的成员变量是容器类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过程就会较复杂,开销也较大。

    选择性的序列化

    在现实应用中,有些时候不能使用默认序列化机制。比如,希望在序列化过程中忽略掉敏感数据,或者简化序列化过程。下面将介绍若干影响序列化的方法。

    使用 transient 关键字

    当类的某个字段被 transient 修饰,默认序列化机制就会忽略该字段。此处将Person类中的age字段声明为transient,如下所示

    public class Person implements Serializable {  
        ...  
        transient private Integer age = null;  
        ...  
    } 
    
    
    // 再执行SimpleSerial应用程序,会有如下输出:
    arg constructor  
    [John, null, MALE]
    

    使用writeObject()方法与readObject()方法

    public class Person implements Serializable {  
        ...  
        transient private Integer age = null;  
        ...  
     
        // writeObject()会先调用ObjectOutputStream中的defaultWriteObject()方法,该方法会执行默认的序列化机制,此时会忽略掉age字段。然后再调用writeInt()方法显示地将age字段写入到        
        // ObjectOutputStream中。
        private void writeObject(ObjectOutputStream out) throws IOException {  
            out.defaultWriteObject();  
            out.writeInt(age);  
        }  
     
        // readObject()的作用则是针对对象的读取,其原理与writeObject()方法相同。
        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
            in.defaultReadObject();  
            age = in.readInt();  
        }  
    } 
    // 再次执行SimpleSerial应用程序,则又会有如下输出:
    arg constructor  
    [John, 31, MALE]
    

    必须注意地是,writeObject()与readObject()都是private方法,那么它们是如何被调用的呢?

    毫无疑问,使用反射。详情可以看看ObjectOutputStream中的writeSerialData方法,以及ObjectInputStream中的readSerialData方法。这两个方法会在序列化、反序列化的过程中被自动调用。且不能关闭流,否则会导致序列化操作失败

    使用 Externalizable 接口

    无论是使用 transient 关键字,还是使用 writeObject() 和 readObject() 方法,其实都是基于 Serializable 接口的序列化。
    Java提供了另一个序列化接口 Externalizable,使用该接口之后,之前基于 Serializable 接口的序列化机制就将失效。
    Externalizable 接口继承于 Serializable 接口,当使用该接口时,序列化的细节需要由程序员去完成。将Person类作如下修改:

    public class Person implements Externalizable {  
        private String name = null;  
        transient private Integer age = null;  
        private Gender gender = null;  
     
        public Person() {  
            System.out.println("none-arg constructor");  
        }  
     
        public Person(String name, Integer age, Gender gender) {  
            System.out.println("arg constructor");  
            this.name = name;  
            this.age = age;  
            this.gender = gender;  
        }  
     
        private void writeObject(ObjectOutputStream out) throws IOException {  
            out.defaultWriteObject();  
            out.writeInt(age);  
        }  
     
        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
            in.defaultReadObject();  
            age = in.readInt();  
        }  
     
        @Override 
        public void writeExternal(ObjectOutput out) throws IOException {  
        }  
     
        @Override 
        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {  
        }  
        ...  
    } 
    
    // 此时再执行SimpleSerial程序,会得到如下结果:
    arg constructor  
    none-arg constructor  
    [null, null, null] 
     
    // 从该结果,一方面可以看出Person对象中任何一个字段都没有被序列化。另一方面,这次序列化过程调用了Person类的无参构造器。
    

    Externalizable 继承于 Serializable,当使用该接口时,序列化的细节需要由程序员去完成。

    如上所示的代码,由于实现的writeExternal()与readExternal()方法未作任何处理,那么该序列化行为将不会保存/读取任何一个字段。这也就是为什么输出结果中所有字段的值均为空。
    另外,使用 Externalizable 接口进行序列化时,读取对象会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中,这就是为什么在此次序列化过程中Person类的无参构造器会被调用。由于这个原因,实现 Externalizable 接口的类必须要提供一个无参构造器,且它的访问权限为public

    对上述Person类做进一步的修改,使其能够对name与age字段进行序列化,但忽略 gender 字段:

    public class Person implements Externalizable {  
        private String name = null;  
        transient private Integer age = null;  
        private Gender gender = null;  
     
        public Person() {  
            System.out.println("none-arg constructor");  
        }  
     
        public Person(String name, Integer age, Gender gender) {  
            System.out.println("arg constructor");  
            this.name = name;  
            this.age = age;  
            this.gender = gender;  
        }  
     
        private void writeObject(ObjectOutputStream out) throws IOException {  
            out.defaultWriteObject();  
            out.writeInt(age);  
        }  
     
        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
            in.defaultReadObject();  
            age = in.readInt();  
        }  
     
        @Override 
        public void writeExternal(ObjectOutput out) throws IOException {  
            out.writeObject(name);  
            out.writeInt(age);  
        }  
     
        @Override 
        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {  
            name = (String) in.readObject();  
            age = in.readInt();  
        }  
        ...  
    } 
     
    // 执行SimpleSerial之后会有如下结果:
    arg constructor  
    none-arg constructor  
    [John, 31, null]
    

    readResolve()方法——单例模式的反序列化

    当使用Singleton模式时,应该是期望某个类的实例应该是唯一的,但如果该类是可序列化的,那么情况可能略有不同。当然目前最好的单例实现方式是使用枚举,如果还是传统的实现方式,才会遇到这个问题。

    serialVersionUID

    serialVersionUID 有什么作用,如何使用 serialVersionUID?
    serialVersionUID 是 Java 为每个序列化类产生的版本标识。它可以用来保证在反序列时,发送方发送的和接受方接收的是可兼容的对象。如果接收方接收的类的 serialVersionUID 与发送方发送的 serialVersionUID 不一致,会抛出 InvalidClassException。

    如果可序列化类没有显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值。尽管这样,还是建议在每一个序列化的类中显式指定 serialVersionUID 的值。因为不同的 jdk 编译很可能会生成不同的 serialVersionUID 默认值,从而导致在反序列化时抛出 InvalidClassExceptions 异常。

    serialVersionUID 字段必须是 static final long类型。
    我们来举个例子:
    (1)有一个可序列化类 Person

    public class Person implements Serializable {
        private static final long serialVersionUID = 1L;
        private String name;
        private Integer age;
        private String address;
        // 构造方法、get、set 方法略
    }
    

    (2)开发过程中,对 Person 做了修改,增加了一个字段 email,如下:

    public class Person implements Serializable {
        private static final long serialVersionUID = 1L;
        private String name;
        private Integer age;
        private String address;
        private String email;
        // 构造方法、get、set 方法略
    }
    

    由于这个类和老版本不兼容,我们需要修改版本号:

    private static final long serialVersionUID = 2L;
    

    再次进行反序列化,则会抛出 InvalidClassException 异常。

    综上所述,我们大概可以清楚:serialVersionUID 用于控制序列化版本是否兼容。若我们认为修改的可序列化类是向后兼容的,则不修改 serialVersionUID。

    序列化和反序列化需要注意的坑

    能序列化的前提

    如果一个类想被序列化,需要实现 Serializable 接口进行自动序列化,或者实现 Externalizable 接口进行手动序列化,否则强行序列化该类的对象,就会抛出 NotSerializableException 异常,这是因为,在序列化操作过程中会对类型进行检查,要求被序列化的类必须属于 Enum、Array 和 Serializable 类型其中的任何一种(Externalizable也继承了Serializable)。

    JVM 是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)

    transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

    FileOutputStream 类有一个带有两个参数的重载 Constructor——FileOutputStream(String, boolean)。若其第二个参数为 true 且 String 代表的文件存在,那么将把新的内容写到原来文件的末尾而非重写这个文件,故不能用这个版本的构造函数来实现序列化,也就是说必须重写这个文件,否则在读取这个文件反序列化的过程中就会抛出异常,导致只有第一次写到这个文件中的对象可以被反序列化,之后程序就会出错。

    要知道序列化的是什么样儿的对象(成员)

    序列化并不保存静态变量

    要想将父类对象也序列化,就需要让父类也实现 Serializable 接口

    若一个类的字段有引用对象,那么在序列化该类的时候不仅该类要实现Serializable接口,这个引用类型也要实现Serializable接口。但有时我们并不需要对这个引用类型进行序列化,此时就需要使用transient关键字来修饰该引用类型保证在序列化的过程中跳过该引用类型。

    通过序列化操作,可以实现对任何可 Serializable 对象的深度复制(deep copy),这意味着复制的是整个对象的关系网,而不仅仅是基本对象及其引用

    如果父类没有实现Serializable接口,但其子类实现了此接口,那么这个子类是可以序列化的,但是在反序列化的过程中会调用父类的无参构造函数,所以在其直接父类(注意是直接父类)中必须有一个无参的构造函数

    序列化的安全性

    服务器端给客户端发送序列化对象数据,序列化二进制格式的数据写在文档中,并且完全可逆。

    一抓包就能就看到类是什么样子,以及它包含什么内容。如果对象中有一些数据是敏感的,比如密码字符串等,则要对字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。

    比如可以通过使用 writeObject 和 readObject 实现密码加密和签名管理,但其实还有更好的方式。

    如果需要对整个对象进行加密和签名,最简单的是将它放在一个 javax.crypto.SealedObject 和/或 java.security.SignedObject 包装器中。两者都是可序列化的,所以将对象包装在 SealedObject 中可以围绕原对象创建一种 “包装盒”。必须有对称密钥才能解密,而且密钥必须单独管理。同样,也可以将 SignedObject 用于数据验证,并且对称密钥也必须单独管理

    反序列化后,何时不是同一个对象

    只要将对象序列化到单一流中,就可以恢复出与我们写出时一样的对象网,而且只要在同一流中,对象都是同一个。
    否则,反序列化后的对象地址和原对象地址不同,只是内容相同

    如果将一个对象序列化入某文件,那么之后又对这个对象进行修改,然后再把修改的对象重新写入该文件,那么修改无效,文件保存的序列化的对象仍然是最原始的。这是因为,序列化输出过程跟踪了写入流的对象,而试图将同一个对象写入流时,并不会导致该对象被复制,而只是将一个句柄写入流,该句柄指向流中相同对象的第一个对象出现的位置。为了避免这种情况,在后续的 writeObject() 之前调用 out.reset() 方法,这个方法的作用是清除流中保存的写入对象的记录

    ArrayList 序列化要注意的问题

    ArrayList实现了java.io.Serializable接口,但是其 elementData 是 transient 的,但是 ArrayList 是通过数组实现的,数组 elementData 用来保存列表中的元素。通过该属性的声明方式知道该数据无法通过序列化持久化。

    但是如果实际测试,就会发现,ArrayList 能被完整的序列化,原因是在writeObject 和 readObject方法中进行了序列化的实现。

    这样设计的原因是因为 ArrayList 是动态数组,如果数组自动增长长度设为 2000,而实际只放了一个元素,那就会序列化 1999 个 null 元素,为了保证在序列化的时候不会将这么多 null 元素序列化,ArrayList 把元素数组设置为transient,但是,作为一个集合,在序列化过程中还必须保证其中的元素可以被持久化,所以,通过重写 writeObject 和 readObject 方法把其中的元素保留下来,具体做法是:

    writeObject方法把elementData数组中的元素遍历到ObjectOutputStream

    readObject方法从ObjectInputStream中读出对象并保存赋值到elementData数组

    序列化问题

    Java 的序列化能保证对象状态的持久保存,但是遇到一些对象结构复杂的情况还是难以处理,这里归纳一下:

    • 父类是 Serializable,所有子类都可以被序列化。
    • 子类是 Serializable ,父类不是,则子类可以正确序列化,但父类的属性不会被序列化(不报错,数据丢失)。
    • 如果序列化的属性是对象,则这个对象也必须是 Serializable ,否则报错。
    • 反序列化时,如果对象的属性有修改或删减,则修改的部分属性会丢失,但不会报错。
    • 反序列化时,如果 serialVersionUID 被修改,则反序列化会失败。

    序列化技术选型

    ava 官方的序列化存在许多问题,因此,建议使用第三方序列化工具来替代。

    Java 官方的序列化主要体现在以下方面:

    • Java 官方的序列无法跨语言使用。

    • Java 官方的序列化性能不高,序列化后的数据相对于一些优秀的序列化的工具,还是要大不少,这大大影响存储和传输的效率。

    • Java 官方的序列化一定需要实现 Serializable 接口。

    • Java 官方的序列化需要关注 serialVersionUID。
      当然我们还有更加优秀的一些序列化和反序列化的工具,根据不同的使用场景可以自行选择!

    • thriftprotobuf - 适用于对性能敏感,对开发体验要求不高

    • hessian - 适用于对开发体验敏感,性能有要求

    • jacksongsonfastjson - 适用于对序列化后的数据要求有良好的可读性(转为 json 、xml 形式)。

    #参考资料

    参考

    Java对象序列化全面总结
    https://www.cnblogs.com/kubixuesheng/p/10344533.html
    深入理解 Java 序列化
    网络传输: 序列化与反序列化

    相关文章

      网友评论

          本文标题:Serializable

          本文链接:https://www.haomeiwen.com/subject/tmpdjktx.html