文章目录

  • 一、序列化的定义
  • 二、实现序列化的方式
  • 1. Serializable
  • 父类与子类的序列化关系
  • 成员是引用的序列化机制
  • 同一对象多次序列化机制
  • 潜在问题
  • 自定义序列化
  • 2. Externalizable
  • 3. 两种序列化对比
  • 序列化版本号
  • 总结


一、序列化的定义

  • 序列化:将对象写入到 IO 流中
  • 反序列化:从 IO 流中恢复对象
  • 使用场景:将对象存入数据库或文件时,在网络通信时传输序列化后的对象时
     

二、实现序列化的方式

如果一个类想要实现序列化,那它就要实现Serializable或者Externalizable两个接口中的一个。这里放个 User 类 用来序列化:

User 类

package com.grh;
import java.io.Serializable;
public class User implements Serializable {

    private String name;
    private String password;

    //get/set省略

    public User() {
        System.out.println("一个对象被创建了");
    }
    public User(String name, String password) {
        System.out.println("一个对象被创建了");
        this.name = name;
        this.password = password;
    }
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

 

1. Serializable

相信对序列化好奇的人都点开过他的源码,然而他的源码是空的,只是一个没有任何函数和属性的接口:

public interface Serializable {
}

那么实现这个接口有什么用呢?

它只是个标记接口,用来提醒 JVM 这个类可以序列化,也就是说, Java 将类是否实现序列化的选择交给了程序员。

我们看个例子,它将 User 类进行序列化,并写入了 User.txt 文件中,然后再从中读出来进行输出。

Test 测试类

package com.grh;
import org.junit.Test;
import java.io.*;
public class serialize {
    @Test
    public void serializeBySerializable() {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("User.txt"));
            User user = new User("张三","123");
            oos.writeObject(user);
            oos.close();

            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("User.txt"));
            User user1 = (User)ois.readObject();
            ois.close();

            System.out.println(user1);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

输出结果:

一个对象被创建了
User{name='张三', password='123'}

我们打开项目所在位置,会发现多了一个 User.txt 文件,里面的内容如下

Java文件反序列化 java反序列化方法_Java文件反序列化


这就是通过字节流序列化后的 User 对象,我们可以看到他的类名:com.grh.User ,也就是说,序列化会将该对象的类的信息也存进去。

还有一个地方值得注意,输出结果中显示只调用了一次构造函数,也就是通过 new 创建对象时创建的,也就是说,JVM 有自己的方式反序列化来创建对象,而不需要通过构造函数。
 

父类与子类的序列化关系

  1. 如果一个类的父类实现了序列化,那么这个类也可以被实例化
  2. 如果一个子类实现了序列化,但父类没有实现,那么将子类反序列化时,会调用父类的无参构造函数构建父类对象,而且父类中的数据都是默认值,如 int 为 0 ,对象为 null。

我们定义一个 Person 类,并用 User 类继承它。

package com.grh;
import java.io.Serializable;
public class Person {
    Person(){
        System.out.println("构建一个person");
    }
}

再次调用会得到一下结果:

构建一个person
一个对象被创建了
构建一个person
User{name='张三', password='123'}

我们可以看到父类 Person 的构造函数调用了两次,一次是 new 的时候,一次是反序列化 user 的时候。
 

成员是引用的序列化机制

如果一个可序列化的类的成员不是基本类型,也不是 String 类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。
 

同一对象多次序列化机制

我们去掉 User 和 Person 的继承关系,将 User 作为 Person 中的一个成员,再加个年龄的成员,Person 类如下

package com.grh;
import java.io.Serializable;
public class Person implements Serializable{
    private int age;
    private User user;

    public Person(){ }
    public Person(int age, User user) {
        this.age = age;
        this.user = user;
    }

    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    public User getUser() { return user; }
    public void setUser(User user) { this.user = user; }
}

再写一个测试类,注意:反序列化的顺序与序列化时的顺序一致:

@Test
    public void serializeBySerializable() {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("User.txt"));
            User setUser = new User("张三","123");
            Person setPerson = new Person(18,setUser);
            Person setPerson1 = new Person(25,setUser);
            oos.writeObject(setUser);
            oos.writeObject(setPerson);
            oos.writeObject(setPerson1);
            oos.writeObject(setPerson1);
            oos.close();

            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("User.txt"));
            User getUser = (User)ois.readObject();
            Person getPerson = (Person)ois.readObject();
            Person getPerson1 = (Person)ois.readObject();
            Person getPerson2 = (Person)ois.readObject();
            ois.close();

            System.out.println(getPerson==getPerson2);     //false
            System.out.println(getPerson.getUser()==getPerson1.getUser());    //true
            System.out.println(getUser==getPerson1.getUser());    //true
            System.out.println(getUser==getPerson2.getUser());    //true

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

可以看到,不管怎么存 user ,读出来时也都是同一个,而年龄不同的 person 是不同的对象,读出来时也是不同的。

Java 序列化同一对象,并不会将此对象序列化多次得到多个对象。

潜在问题

每个序列化的对象都有一个编号,如果多次序列化,只会重复这个编号,并不会重新序列化。

因此,如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,只是保存序列化编号,会导致更改无效化。如:

@Test
    public void serializeBySerializable() {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("User.txt"));
            User setUser = new User("张三","123");
            oos.writeObject(setUser);
            setUser.setName("王五");
            oos.writeObject(setUser);
            oos.close();

            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("User.txt"));
            User getUser = (User)ois.readObject();
            ois.close();

            System.out.println(getUser);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

输出结果:

User{name='张三', password='123'}

可以看到修改名字为王五并没有实现。
 

自定义序列化

  • static 不会被序列化,因为序列化存的是对象,static 所代表的是类
  • 在成员前加上 transient 关键字,就不会对这个成员进行序列化,反序列化时还会有这个成员,不过会赋值默认值。
  • 重写 writeObject 与 readObject 方法,就是修改序列化的规则,比 transient 更灵活,还可以对数据进行加密,提高安全性。注意,writeObject 与 readObject 方法需要对应,怎么进行序列化,就应该怎样反序列化。如:
public class Person implements Serializable {

    private String name;
    private int age;

    //省略构造方法,get及set方法
    private void writeObject(ObjectOutputStream out) throws IOException {
        //将名字反转写入二进制流
        out.writeObject(new StringBuffer(this.name).reverse());
        out.writeInt(age);
    }

    private void readObject(ObjectInputStream ins) throws IOException, ClassNotFoundException {
        //将读出的字符串反转恢复回来
        this.name = ((StringBuffer) ins.readObject()).reverse().toString();
        this.age = ins.readInt();
    }
}
  • 重写 writeReplace 和 readResolve 方法,这两个方法会在写入之前调用,一般用于替换掉要写入或读取的对象。
package com.grh;
import java.io.*;
import java.util.ArrayList;
public class Person implements Serializable {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    private Object writeReplace() throws ObjectStreamException {
        ArrayList<Object> list = new ArrayList<Object>(2);
        list.add(this.name);
        list.add(this.age);
        return list;
    }

    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
            ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"));
            Person person = new Person("张三", 23);
            oos.writeObject(person);
            ArrayList list = (ArrayList) ios.readObject();
            System.out.println(list);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
//输出结果:
//[张三, 23]

在上面的例子中,本来写入的是 Person,但换成了 list,读取出来的时候也可以看到已经是 list 类型而不是 Person 类型了。

package com.grh;
import java.io.*;
import java.util.HashMap;

public class Person implements Serializable {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

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

    private Object readResolve() throws ObjectStreamException {
        return new Person("李四", 23);
    }

    public static void main(String[] args){
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
            ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"));
            Person person1 = new Person("张三", 23);
            Person person2 = new Person("王五", 23);
            oos.writeObject(person1);
            oos.writeObject(person2);

            Person person3 = (Person) ios.readObject();
            Person person4 = (Person) ios.readObject();

            System.out.println(person3);
            System.out.println(person4);
            System.out.println(person3==person4);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在这个类中,无论序列化了多少个对象进入文件中,当他读出的时候,始终会调用 readResolve() 方法 new 一个李四的对象出来,序列化中的数据已经不重要了。

一般来说,重写 readResolve() 主要用于防止破坏单例模式的规则,因为反序列化后会重新构建一个一样的对象,如果在 readResolve() 方法中直接返回已经存在的单例对象,反序列化后就不会生成对象了。

注:writeReplace() 在writeObject前调用,readResolve() 在 readObject() 后调用。
 

2. Externalizable

Externalizable接口中,有两个方法,writeExternal() 和 readExternal() ,因此要想实现这个接口来序列化,必须实现这两个函数

package com.grh;
import java.io.*;
public class ExPerson implements Externalizable {
    private String name;
    private int age;
    //注意,必须加上pulic 无参构造器
    public ExPerson() {  
    	System.out.println("无参构造函数构建对象");
    }
    public ExPerson(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "ExPerson{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    public void writeExternal(ObjectOutput out) throws IOException {
        //将name反转后写入二进制流
        StringBuffer reverse = new StringBuffer(name).reverse();
        System.out.println(reverse.toString());
        out.writeObject(reverse);
        out.writeInt(age);
    }
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        //将读取的字符串反转后赋值给name实例变量
        this.name = ((StringBuffer) in.readObject()).reverse().toString();
        System.out.println(name);
        this.age = in.readInt();
    }
    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ExPerson.txt"));
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ExPerson.txt"));
                        oos.writeObject(new ExPerson("brady", 23));
                        ExPerson ep = (ExPerson) ois.readObject();
                        System.out.println(ep);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

输出结果:

ydarb
无参构造函数构建对象
brady
ExPerson{name='brady', age=23}

这个例子跟 Serializable 接口中的自定义序列化基本一致,将用户名反转再序列化和反序列化。

注意:Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供pulic的无参构造器,因为在反序列化的时候需要反射创建对象。

3. 两种序列化对比

Serializable

Externalizable

系统自动存储必要的信息

程序员决定存储哪些信息

Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持

必须实现接口内的两个方法

性能略差

性能略好

虽然Externalizable接口带来了一定的性能提升,但变成复杂度也提高了,所以一般通过实现Serializable接口进行序列化。
 

序列化版本号

java序列化提供了一个private static final long serialVersionUID 的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。

如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常。

序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。

什么情况下需要修改serialVersionUID呢?分三种情况:

  • 如果只是修改了方法,反序列化不容影响,则无需修改版本号;
  • 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;
  • 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。
     

总结

  • 所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。
  • 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
  • 如果想让某个变量不被序列化,使用transient修饰。
  • 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
  • 反序列化时必须有序列化对象的class文件。
  • 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
  • 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。
  • 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
  • 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。