资讯专栏INFORMATION COLUMN

Java ---- 序列化

zorpan / 3031人阅读

摘要:使用对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。由此可知,对象序列化不会关注类中的静态变量。对象的读写类中对象的序列化工作是通过和来完成的。这就是为什么在此序列化过程中的无参构造器会被调用。

Java对象的序列化

Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。

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

简而言之,就是让对象想基本数据类型和字符串类型一样,通过输入输出字节流ObjectInputStream 和 ObjectOutputStream进行写和读操作。

Java序列化的应用场景

当你想把的内存中的对象状态保存到一个文件中或者数据库中时候

当你想用套接字在网络上传送对象的时候

当你想通过RMI传输对象的时候

如何对Java对象进行序列化与反序列化

在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。

import java.io.*;
import java.util.*;

class User implements Serializable {
    private String name;
    private int age;
    private Date birthday;
    private transient String gender;
    private static int test =1;
    private static final long serialVersionUID = -6849794470754667710L;

    public User() {
        System.out.println("none-arg constructor");
    }

    public User(String name, Integer age,Date birthday,String gender) {
        System.out.println("arg constructor");
        this.name = name;
        this.age = age;
        this.birthday = birthday;
        this.gender = gender;
    }

    public void setTest(int Newtest) {
        this.test = Newtest;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "User{" +
                "name="" + name + """ +
                ", age=" + age +
                ", gender=" + gender +
                ", birthday=" + birthday +
                ", testStatic="+test+
                "}"+" "+super.toString();
    }
}

public class SerializableDemo {
    public static void main(String[] args) throws Exception {
        //Initializes The Object
        User user = new User("qiuyu",23,new Date(),"male");
        System.out.println(user);
        user.setTest(10);
        System.out.println(user);

        //Write Obj to File
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("tempFile"));
        out.writeObject(user);
        out.close();

        //Read Obj from File
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("tempFile"));
        User newUser = (User) in.readObject();
        System.out.println(newUser);
        in.close();
    }
}

此时的输出为:

arg constructor
User{name="qiuyu", age=23, gender=male, birthday=Tue Nov 14 20:38:57 GMT+08:00 2017, testStatic=10} User@326de728
User{name="qiuyu", age=23, gender=null, birthday=Tue Nov 14 20:38:57 GMT+08:00 2017, testStatic=10} User@4883b407

此时必须注意的是,当重新读取被保存的User对象时,并没有调用User的任何构造器,看起来就像是直接使用字节将User对象还原出来的。

当User对象被保存到tempfile文件中之后,我们可以在其它地方去读取该文件以还原对象,但必须确保该读取程序的CLASSPATH中包含有User.class,否则会抛出ClassNotFoundException。

Q:之前不是说序列化不保存静态变量么,为什么这里的静态变量进行了传递,都变成了10呢?
A:因为此时User.class已经被加载进了内存中,且将static变量test从1更改为了10。当我们恢复对象时,会直接获取当前static的变量test的值,所以为10。

Q:但如果我们的恢复操作在另一个文件中进行,结果会怎么样呢?

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class Recovering {
    public static void main(String[] args) throws Exception{
        //Read Obj from File
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("tempFile"));
        User newUser = (User) in.readObject();
        System.out.println(newUser);
        in.close();
    }
}

输出结果为:

User{name="qiuyu", age=23, gender=null, birthday=Tue Nov 14 20:38:57 GMT+08:00 2017, testStatic=1} User@6442b0a6

A:因为在运行此代码时,User.class会被加载进内存,然后执行初始化,将被初始化为1,因此test变量会被恢复为1。注意区分上面两种情况,不要因为是在本地进行测试,就认为static会被序列化,同时要了解Java运行时,内存加载的机制。

基本知识点

Serializable接口

对于任何需要被序列化的对象,都必须要实现接口Serializable,它只是一个标识接口本身没有任何成员,只是用来标识说明当前的实现类的对象可以被序列化.

如果父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口。

如果被写对象的类型是String数组EnumSerializable,那么就可以对该对象进行序列化,否则将抛出NotSerializableException。

对象的读写

Java类中对象的序列化工作是通过 ObjectOutputStream ObjectInputStream 来完成的。

使用readObject()writeObject()方法对对象进行读写操作;

对于基本类型,可以使用readInt()writeInt() , readDouble()writeDouble()等类似的接口进行读写。

序列化机制

如果仅仅只是让某个类实现Serializable接口,而没有其它任何处理的话,则就是使用默认序列化机制。使用默认机制,在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化。

在现实应用中,有些时候不能使用默认序列化机制。比如,希望在序列化过程中忽略掉敏感数据,或者简化序列化过程。为此需要为某个字段声明为transient,那么序列化机制就会忽略被transient修饰的字段。transient的引用变量会以null返回,基本数据类型会以相应的默认值返回。(例如:引用类型没有实现Serializable,或者动态数据只可以在执行时求出而不能或不必存储)

writeObject()与readObject()

对于上被声明为transient的字段gender,除了将transient关键字去掉之外,是否还有其它方法能使它再次可被序列化?方法之一就是在User类中添加两个方法:writeObject()与readObject(),如下所示:

private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeUTF(gender);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        gender = in.readUTF();
    }

在writeObject()方法中会先调用ObjectOutputStream中的defaultWriteObject()方法,该方法会执行默认的序列化机制,此时会忽略掉gender字段。

然后再调用writeUtf()方法显示地将gender字段写入到ObjectOutputStream中。readObject()的作用则是针对对象的读取,其原理与writeObject()方法相同。

Q: writeObject()与readObject()都是private方法,那么它们是如何被调用的呢?
A: 毫无疑问,是使用反射。(注意这不是继承接口的方法,因为接口类的方法都是public的,而这里的方法是private的)

Externalizable接口

无论是使用transient关键字,还是使用writeObject()和readObject()方法,其实都是基于Serializable接口的序列化。JDK中提供了另一个序列化接口Externalizable,使用该接口之后,之前基于Serializable接口的序列化机制就将失效。

import java.io.*;
import java.util.*;

class UserExtern implements Externalizable {
    private String name;
    private int age;
    private Date birthday;
    private transient String gender;
    private static int test =1;
    private static final long serialVersionUID = -6849794470754667710L;

    public UserExtern() {
        System.out.println("none-arg constructor");
    }

    public UserExtern(String name, Integer age,Date birthday,String gender) {
        System.out.println("arg constructor");
        this.name = name;
        this.age = age;
        this.birthday = birthday;
        this.gender = gender;
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeUTF(gender);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        gender = in.readUTF();
    }


    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeInt(age);
        out.writeObject(birthday);
        out.writeUTF(gender);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = in.readUTF();
        age = in.readInt();
        birthday = (Date) in.readObject();
        gender = in.readUTF();
    }


    public void setTest(int Newtest) {
        this.test = Newtest;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "User{" +
                "name="" + name + """ +
                ", age=" + age +
                ", gender=" + gender +
                ", birthday=" + birthday +
                ", testStatic="+test+
                "}"+" "+super.toString();
    }
}



public class ExternalizableDemo {
    public static void main(String[] args) throws Exception {
        UserExtern userExtern = new UserExtern("qiuyu",23,new Date(),"male");
        System.out.println(userExtern);
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("external"));
        out.writeObject(userExtern);
        out.close();


        ObjectInputStream in = new ObjectInputStream(new FileInputStream("external"));
        UserExtern userExtern1 = (UserExtern)in.readObject();
        System.out.println(userExtern1)
    }
}

输出结果为:

arg constructor
User{name="qiuyu", age=23, gender=male, birthday=Tue Nov 14 22:34:25 GMT+08:00 2017, testStatic=1} UserExtern@25618e91
none-arg constructor
User{name="qiuyu", age=23, gender=male, birthday=Tue Nov 14 22:34:25 GMT+08:00 2017, testStatic=1} UserExtern@604ed9f0

Externalizable继承于Serializable,当使用该接口时,序列化的细节需要由程序员去完成writeExternal()readExternal()方法的具体细节,以及哪些状态需要进行序列化。

另外,若使用Externalizable进行序列化,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。这就是为什么在此序列化过程中UserExtern的无参构造器会被调用。由于这个原因,实现Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public

注意事项

读取对象的顺序必须与写入的顺序相同

如果有不能被序列化的对象,执行期间就会抛出NotSerializableException异常

序列化时,只对对象的状态进行保存,而不管对象的方法

静态变量不会被序列化,因为所有的对象共享同一份静态变量的值

如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存还原,而且会是递归的方式(对象网)。(序列化程序会将对象版图上的所有东西储存下来,这样才能让该对象恢复到原来的状态)

如果子类实现Serializable接口而父类未实现时,父类不会被序列化,但此时父类必须有个无参构造方法,否则会抛InvalidClassException异常;因为反序列化时会恢复原有子对象的状态,而父类的成员变量也是原有子对象的一部分。由于父类没有实现序列化接口,即使没有显示调用,也会默认执行父类的无参构造函数使变量初始化;

深入理解

序列化ID的问题

serialVersionUID适用于JAVA的序列化机制。简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。

在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。

序列化存储规则

Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用;

序列化到同一个文件时,如第二次修改了相同对象属性值再次保存时候,虚拟机根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用,所以读取时,都是第一次保存的对象,第二次进行的修改将无效。

    public static void main(String[] args) throws Exception {
        //Initializes The Object
        User user = new User("qiuyu",23,new Date(),"male");
        user.setTest(10);
        System.out.println(user);


        //Write Obj to File
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("tempFile"));
        out.writeObject(user);
        user.setAge(25);
        System.out.println(user);
        out.writeObject(user);
        out.close();

        //Read Obj from File
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("tempFile"));
        User newUser = (User) in.readObject();
        User newUser1 = (User) in.readObject();
        System.out.println(newUser);
        System.out.println(newUser1);
        in.close();
    }

输出结果: 注意观察age的值

User{name="qiuyu", age=23, gender=male, birthday=Tue Nov 14 22:47:22 GMT+08:00 2017, testStatic=10} User@326de728
User{name="qiuyu", age=25, gender=male, birthday=Tue Nov 14 22:47:22 GMT+08:00 2017, testStatic=10} User@326de728
User{name="qiuyu", age=23, gender=male, birthday=Tue Nov 14 22:47:22 GMT+08:00 2017, testStatic=10} User@4883b407
User{name="qiuyu", age=23, gender=male, birthday=Tue Nov 14 22:47:22 GMT+08:00 2017, testStatic=10} User@4883b407

多次序列化的问题

在一次的序列化的过程中,ObjectOutputStream 会在文件开始的地方写入一个 Header的信息到文件中。于是在多次序列化的过程中就会继续在文件末尾(本次序列化的开头)写入 Header 的信息,这时如果进行反序列化的对象的时候会报错:java.io.StreamCorruptedException: invalid type code: AC

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68007.html

相关文章

  • 小伙子,你真的搞懂 transient 关键字了吗?

    摘要:由以上结果分析可知,静态变量不能被序列化,示例读取出来的是在内存中存储的值。关键字总结修饰的变量不能被序列化只作用于实现接口只能用来修饰普通成员变量字段不管有没有修饰,静态变量都不能被序列化好了,栈长花了半天时间,终于整理完了。 先解释下什么是序列化 我们的对象并不只是存在内存中,还需要传输网络,或者保存起来下次再加载出来用,所以需要Java序列化技术。 Java序列化技术正是将对象转...

    curlyCheng 评论0 收藏0
  • Java 对象列化

    摘要:对象序列化对象序列化机制允许把内存中的对象转换成与平台无关的二进制流,从而可以保存到磁盘或者进行网络传输,其它程序获得这个二进制流后可以将其恢复成原来的对象。 对象序列化 对象序列化机制允许把内存中的Java对象转换成与平台无关的二进制流,从而可以保存到磁盘或者进行网络传输,其它程序获得这个二进制流后可以将其恢复成原来的Java对象。 序列化机制可以使对象可以脱离程序的运行而对立存在 ...

    tianyu 评论0 收藏0
  • Java开发中对象的列化与反列化

    摘要:在中,对象的序列化与反序列化被广泛应用到远程方法调用及网络传输中。相关接口及类为了方便开发人员将对象进行序列化及反序列化提供了一套方便的来支持。未实现此接口的类将无法使其任何状态序列化或反序列化。 序列化与反序列化 序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。一般将一个对象存储至一个储存媒介,例如档案或是记亿体缓冲等。在网络传输过程中,可以...

    fox_soyoung 评论0 收藏0
  • Java列化

    摘要:的序列化是将一个对象表示成字节序列,该字节序列包括了对象的数据,有关对象的类型信息和存储在对象中的数据类型。任何实现了接口的类都可以被序列化。一旦对象被序列化或者重新装配,就会分别调用那两个方法。 Java序列化 1. 什么是序列化? 序列化是将一个对象的状态,各属性的值序列化保存起来,然后在合适的时候通过反序列化获得。 Java的序列化是将一个对象表示成字节序列,该字节序列包括了对象...

    lbool 评论0 收藏0
  • 浅谈Java列化

    摘要:的序列化是将一个对象表示成字节序列,该字节序列包括了对象的数据,有关对象的类型信息和存储在对象中的数据类型。这个是根据类名接口名成员方法及属性等来生成一个位的哈希字段,因为增加了字段,因此生成的不一样了。 Java序列化 什么是序列化? 序列化是将一个对象的状态,各属性的值序列化保存起来,然后在合适的时候通过反序列化获得。 Java的序列化是将一个对象表示成字节序列,该字节序列包括了对...

    winterdawn 评论0 收藏0
  • java列化和反列化说起

    摘要:从的序列化和反序列化说起序列化是将对象的状态信息转换为可以存储或传输的形式的过程,而相反的过程就称为反序列化。当使用接口来进行序列化与反序列化的时候需要开发人员重写与方法。 从java的序列化和反序列化说起 序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程,而相反的过程就称为反序列化。 在java中允许我们创建可复用的对象,但是这些对象仅仅存在j...

    whlong 评论0 收藏0

发表评论

0条评论

zorpan

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<