Java序列化与反序列化

  • 概述

       在Java开发过程中,我们常常需要将JVM中的对象持久化下来,并且能够在需要的时候把对象重新读取出来。数据库可以帮助我们解决这个问题,另外Java原生的对象序列化也可以帮助我们实现该功能,这里我们深入了解一下序列化的知识。在实际应用场景中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。

  • Serializable接口

  1. 类通过实现 java.io.Serializable 接口以启用其序列化功能;
  2. 如果要序列化的类有父类,要想同时将在父类中定义过的变量持久化下来,那么父类也应该继承java.io.Serializable接口。
public class Person implements Serializable{
    private String name;

    private Integer age;

    private Education education;

    public Person() {
        System.out.println("default constructor");
    }

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

    @Override
    public String toString(){
        return "Person{name=" + name + ", age=" + age + ", school=" + education.getSchool() + "}";
    }
}
public static void main(String[] args) throws Exception {
    Person person = new Person("kobe",24);
    Education education = new Education();
    education.setSchool("浙江大学");
    person.setEducation(education);
    System.out.println(person);

    File file = new File("C:\\Users\\Administrator\\Desktop\\person.txt");
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
    out.writeObject(person);
    out.close();

    ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
    Person newPerson = (Person)in.readObject();
    in.close();
    System.out.println(newPerson);
}
输出结果:
arg constructor
Person{name=kobe, age=24, school=浙江大学}
Person{name=kobe, age=24, school=浙江大学}

       这里有两点是值得注意的,一是如果Education未实现Serializable接口,会抛出java.io.NotSerializableException;二是当反序列化Person对象时,并没有调用Person的任何构造器,看起来就像是直接使用字节将Person对象还原出来的。

  • 序列化的控制

       通过Serializable 接口都是按照默认机制实现序列化,有些场景下,我们希望能控制序列化的机制。比如age字段被认为是敏感数据,我们希望不被持久化下来,又或者持久化过程中对该字段进行加密。那么有哪些方法可以实现呢?

  • transient关键字

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

public class Person implements Serializable {
    private String name;

    transient private Integer age;

    public Person() {
        System.out.println("default constructor");
    }

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

    @Override
    public String toString(){
        return "Person{name=" + name + ", age=" + age + "}";
    }
}
public static void main(String[] args) throws Exception {
    Person person = new Person("kobe",24);
    System.out.println(person);

    File file = new File("C:\\Users\\Administrator\\Desktop\\person.txt");
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
    out.writeObject(person);
    out.close();

    ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
    Person newPerson = (Person)in.readObject();
    in.close();
    System.out.println(newPerson);
}
输出结果:
arg constructor
Person{name=kobe, age=24}
Person{name=kobe, age=null}

       我们熟悉使用 Transient 关键字可以使得字段不被序列化,那么还有别的方法吗?根据父类对象序列化的规则,我们可以将不需要被序列化的字段抽取出来放到父类中,子类实现 Serializable 接口,父类不实现,根据父类序列化规则,父类的字段数据将不被序列化。

  • Externalizable

       除了Serializable 之外,java中还提供了另一个序列化接口Externalizable。Externalizable继承了Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法。我们把上面的例子改成实现Externalizable接口:

public class Person implements Externalizable {
    private String name;

    private Integer age;

    public Person() {
        System.out.println("default constructor");
    }

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

    @Override
    public String toString(){
        return "Person{name=" + name + ", age=" + age + "}";
    }

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

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        //这里的读顺序要和writeExternal中的写顺序保持一致
        name = (String) in.readObject();
        age = (Integer) in.readObject();
    }
}
public static void main(String[] args) throws Exception {
    Person person = new Person("kobe",24);
    System.out.println(person);

    File file = new File("C:\\Users\\Administrator\\Desktop\\person.txt");
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
    out.writeObject(person);
    out.close();

    ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
    Person newPerson = (Person)in.readObject();
    in.close();
    System.out.println(newPerson);
}
输出结果:
arg constructor
Person{name=kobe, age=24}
default constructor
Person{name=kobe, age=24}

       由此可见,Externalizable接口亦可以实现序列化和反序列化。在writeExternal()与readExternal()方法中可以手动指定要序列化的字段,并且两个方法中的字段读写顺序应该完全保持一致。如果不指定字段,则不会被序列化:

public class Person implements Externalizable {
    private String name;

    private Integer age;

    public Person() {
        System.out.println("default constructor");
    }

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

    @Override
    public String toString(){
        return "Person{name=" + name + ", age=" + age + "}";
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        
    }
}
public static void main(String[] args) throws Exception {
    Person person = new Person("kobe",24);
    System.out.println(person);

    File file = new File("C:\\Users\\Administrator\\Desktop\\person.txt");
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
    out.writeObject(person);
    out.close();

    ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
    Person newPerson = (Person)in.readObject();
    in.close();
    System.out.println(newPerson);
}
输出结果:
arg constructor
Person{name=kobe, age=24}
default constructor
Person{name=null, age=null}

       我们看到在writeExternal()与readExternal()方法中不指定字段,那么就不会被序列化,反序列化之后就被赋为默认值。因此我们可以在这两个方法中自由控制序列化内容,并可以在序列化之前做一些特殊处理,比如对敏感字段加密。

       一个有意思的地方是输出结果中打印出了default constructor,我们发现通过Externalizable接口反序列化的过程中调用了默认的无参构造方法。原来在使用Externalizable进行反序列化时,会调用类的无参构造方法去创建一个新对象,然后通过writeExternal()与readExternal()方法将字段的值填充到新对象中。由于这个原因,实现Externalizable接口的类必须要提供一个无参构造方法,且它的访问权限要为public。

  • writeObject()与readObject()

       实现Externalizable 接口可以控制序列化,其实使用Serializable 接口也可以达到相同的目的,只要添加writeObject()与readObject()方法:

public class Person implements Serializable {
    private String name;

    private Integer age;

    public Person() {
        System.out.println("default constructor");
    }

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

    @Override
    public String toString(){
        return "Person{name=" + name + ", age=" + age + "}";
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeObject(name);
        out.writeObject(age);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        name = (String) in.readObject();
        age = (Integer) in.readObject();
    }
}
public static void main(String[] args) throws Exception {
    Person person = new Person("kobe",24);
    System.out.println(person);

    File file = new File("C:\\Users\\Administrator\\Desktop\\person.txt");
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
    out.writeObject(person);
    out.close();

    ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
    Person newPerson = (Person)in.readObject();
    in.close();
    System.out.println(newPerson);
}
输出结果:
arg constructor
Person{name=kobe, age=24}
Person{name=kobe, age=24}

       所以实现Serializable 接口并且在类中添加writeObject()与readObject()方法,也可以达到与实现Externalizable 接口相同的目的,两者都能控制序列化过程。请注意这里说的是添加writeObject()与readObject()方法,而不是覆盖或者实现,并且这两个方法还被定义成了private,那么我们能想到的就是它们是通过反射被调用的。事实上,在ObjectOutputStream 和ObjectInputStream 的writeObject()与readObject()方法中通过反射调用了Person类中自定义的writeObject()与readObject()方法。

  • 序列化ID

       虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。序列化ID在IDEA下提供了两种生成策略,一个是固定的1L,一个是随机生成一个不重复的long类型数据(实际上是使用JDK工具生成),在这里有一个建议,如果没有特殊需求,就是用默认的1L就可以,这样可以确保代码一致时反序列化成功。

       当类中没有显示定义serialVersionUID字段,虚拟机会根据编译时的class自动生成一个serialVersionUID。如果后续该类发生变更,加了字段甚至是加了空格,都会使虚拟机重新生成一个serialVersionUID,这时反序列化便会出现serialVersionUID不一致,导致反序列化失败。所以建议要有序列化需求的实体类都要显示定义serialVersionUID字段。

  • 静态变量序列化

public class Person implements Serializable {
    private String name;

    private Integer age;

    private static int staticVar = 1;

    public Person() {
        System.out.println("default constructor");
    }

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

    @Override
    public String toString(){
        return "Person{name=" + name + ", age=" + age + ", staticVar=" + staticVar + "}";
    }
}
public static void main(String[] args) throws Exception {
    Person person = new Person("kobe",24);
    System.out.println(person);

    File file = new File("C:\\Users\\Administrator\\Desktop\\person.txt");
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
    out.writeObject(person);
    out.close();

    person.setName("T-mac");
    person.setAge(23);
    staticVar = 2;

    ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
    Person newPerson = (Person)in.readObject();
    in.close();
    System.out.println(newPerson);
}
输出结果:
arg constructor
Person{name=kobe, age=24, staticVar=1}
Person{name=kobe, age=24, staticVar=2}

       这个例子中,person对象被序列化后我们修改了它的状态以及类的静态变量,反序列化后发现状态值的修改不生效,而静态变量的修改却生效了,就是说静态变量没有从序列化文件中恢复出来。原来,序列化时并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值