java序列化机制详解

java序列化机制详解

java序列化是将java对象保存在文件或者通过网络传输的机制,通过实现接口Serializable或者Externalizable标识该类的对象可以序列化和反序列化。

如果希望保存java对象的状态并在以后的某个时刻在内存中重建该对象,我们可以通过java序列化的机制实现。

Serializable关键字

标识类对象是可序列化的只要实现Serializable接口即可,该接口不包括任何字段和方法,它只是一个空的接口。如以下代码片断,Student类实现了该接口并自动获得了序列化的能力。

//实现了Serializable接口的类自动拥有序列化的能力
public class Student implements Serializable {
}

对象的状态是由成员变量决定的,所以序列化保存的是成员变量的值。static修饰的是类变量,序列化的时候static变量不会保存,另外,我们还可以通过java提供的transient关键字来显示标识变量不需要序列化。因此java序列化的是除了static和transient修饰的其它成员变量。

ObjectOutputSteram和ObjectInputStream

如何使用java的序列化机制呢?

java提供了ObjectOutputStream和ObjectInputStream两个对象输入输出流来实现序列化。假设Student类需要序列化,类的定义如下:

import java.io.Serializable;

/**
 * <p>文件描述: 需要序列化的类需要实现Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午7:30
*/
public class Student implements Serializable {

    private Integer age;
    private String name;

    public Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

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

下面代码是将Student对象序列化的例子

该例中将对象序列化到了文件student.out中,通过ObjectOutputStream将对象jack写到文件输出流中。接着通过ObjectInputStream将jack对象反序列化并强制转换为Student类。(注意此时需要捕获ClassNotFoundException类,假如此时JVM找不到Student Class对象,将会抛出该异常。)

import java.io.*;

/**
 * <p>文件描述: 对象序列化Demo</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午7:34
*/
public class SerializableDemo {
    public static void main(String[] args) {
        try {
            /**
             * 对象输出流,具体输出流是文件输出
             */
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
            Student jack = new Student(23, "jack");
            /**
             * 通过ObjectOutputStream将对象写入到文件输出流
             */
            oos.writeObject(jack);
            oos.close();
            /**
             * 对象输入流,具体输入是文件输入流
             */
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
            /**
             * 通过ObjectInputStream将对象从文件输入流读入
             */
            Student stu = (Student) ois.readObject();
            ois.close();
            System.out.println(stu);

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

输出:

Student{age=23, name='jack'}

若Student类未实现Serializable接口,将会抛出运行时异常 java.io.NotSerializableException。

transient关键字

若将Student的age字段设置为transient的,反序化后age字段将是空值:

import java.io.Serializable;

/**
 * <p>文件描述: 需要序列化的类需要实现Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午7:40
*/
public class Student implements Serializable{

    //transient修饰的字段默认不会被序列化     
    private transient Integer age;
    private String name;

    public Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

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

Demo运行后的输出为:

Student{age=null, name='jack'}

可以看到输出后的age为null,transient修饰的字段将不会被序列化。

static修饰的成员变量

前面我们说过java序列化包括的是除了static和transient修饰的其它成员变量。假设将Student的name字段设置为static,看下Demo输出结果:

import java.io.Serializable;

/**
 * <p>文件描述: 需要序列化的类需要实现Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午7:55
*/
public class Student implements Serializable{

    private Integer age;
    private static String name = "student";

    public Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

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

Demo输出结果为:

Student{age=23, name='jack'}

看到反序列化后name的值是”jack”而不是”student”,这是为何?

这正是因为不会序列化static修饰的变量而导致的结果。序列化后的文件其实name字段值为空,反序列化后去方法区查找静态变量name的值,这个name值正是之前Demo类通过Student的构造函数将name值设置为”jack”的值,所以最终反序列化后的name值为”jack”。

也就是说这个”name”值不是通过反序列化得到的,而是通过去方法区拿到的全局的值。

如果读者对上例有疑惑,可以继续看下面的例子:

import java.io.*;

/**
 * <p>文件描述: 需要序列化的类需要实现Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午20:30
*/
public class Student implements Serializable{

    private Integer age;
    private static String name = "student";

    public Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }
    //增加了设置静态变量的方法
    public static void setName(String name) {
        Student.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}
import java.io.*;

/**
 * <p>文件描述: </p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午20:52
*/
public class SerializableDemo {
    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
            Student jack = new Student(23, "jack");
            oos.writeObject(jack);
            oos.close();
           //反序列化前将静态变量设置为"tom"
            Student.setName("tom");

            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
            Student stu = (Student) ois.readObject();
            ois.close();
            System.out.println(stu);

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

Demo输出结果为:

Student{age=23, name='tom'}

输出结果的name值为”tom”,而不是写入时的”jack”。因为在反序列化之前已经将静态变量name的值设置为”tom”,反序列化后会去取这个静态变量的值。:smile:

writeObject()和readObject()方法

如果没有为类添加这两个方法,序列化和反序列化是通过ObjectOutputStream的defaultWriteObject()和ObjectInputStream的defaultReadObject()方法进行的。

如果为类添加writeObject()和readObject()方法,将不会再调用ObjectOutputStream的defaultWriteObject()和ObjectInputStream的defaultReadObject()方法,我们可以更加精确的操作对象的序列化。

例如如果将Student类的age字段设置为transient了,此时我们又想去序列化该字段,那么我们通过这两个方法来实现:

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * <p>文件描述: 需要序列化的类需要实现Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午21:12
*/
public class Student implements Serializable{

    private transient Integer age;
    private String name;

    public Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
        //默认的序列化
        oos.defaultWriteObject();
        oos.writeInt(age);
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        //默认的反序列化
        ois.defaultReadObject();
        this.age = ois.readInt();
    }

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

此时Demo的输出结果将会输出age的值:

Student{age=23, name='jack'}

注意,这两个方法都是private的。方法中首先调用了序列化的默认方法,之后才是自定义的逻辑。还要注意的是序列化的顺序和反序列化的顺序必须一致。否则会读出未知的结果。例如将Student的age和name都设置为transient,在writeObject和readObject方法中调整写入和读出的顺序:

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * <p>文件描述: 需要序列化的类需要实现Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午21:29
*/
public class Student implements Serializable{

    private transient Integer age;
    private transient String name;

    public Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
        //默认的序列化
        oos.defaultWriteObject();
        oos.writeInt(age);
        oos.writeUTF(name);
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        //默认的序列化
        ois.defaultReadObject();
        //此处读入的顺序和写入的顺序不一致
        this.name = ois.readUTF();
        this.age = ois.readInt();
    }

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

Demo输出结果让人莫名其妙:

Student{age=1507332, name=''}

此处如果将oos.writeUTF方法改为oos.writeObject、ois.readUTF方法改为ois.readObject,会抛出异常 OptionalDataException

Externalizable关键字

实现了该接口的类基于之前Serializable的序列化机制就会失效,Externalizable接口已经实现了Serializable。看下面的例子:

import java.io.*;

/**
 * <p>文件描述: 需要序列化的类需要实现Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午21:53
*/
public class Student implements Externalizable{

    private Integer age;
    private String name;

    public Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

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

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

    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {

    }
}

将Student改为上面的代码,运行Demo类的输出结果为:

java.io.InvalidClassException: com.lms.serializable.Student; no valid constructor

异常抛出,提示没有可用的构造器,其实就是没有默认的构造函数。实现了Externalizable接口的类对象,反序列化的时候会先通过无参的构造函数new一个对象,然后将各个字段的值赋值给该对象,Student没有默认的构造函数,所以上例抛出异常。

为Student添加默认构造函数后的Demo输出结果为:

Student{age=null, name='null'}

可以看出,所有字段为空,这是因为实现的Externalizable接口的对象序列化需要开发者自己去保证,也就是需要实现writeExternal和readExternal方法,实现了writeExternal和readExternal方法后的Student类如下:

import java.io.*;

/**
 * <p>文件描述: 需要序列化的类需要实现Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午22:14
*/
public class Student implements Externalizable{

    private Integer age;
    private String name;

    public Student() {

    }

    public Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

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

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

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

Demo输出结果为:

Student{age=23, name='jack'}

可以看出正确输出了结果。

单例的序列化

如果一个类是单例,反序列化后是同一个对象吗?我们将Student改为单例模式的(Student用单例模式确实有点牵强),看下例:

import java.io.*;

/**
 * <p>文件描述: 需要序列化的类需要实现Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午22:40
*/
public class Student implements Serializable{

    private Integer age;
    private String name;

    private static class Instance {
        private static Student jack = new Student(23, "jack");
    }

    private Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    public static Student getInstance() {
        return Instance.jack;
    }

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

}

Demo类修改为:

import java.io.*;

/**
 * <p>文件描述:Demo类 </p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午23:00
*/
public class SerializableDemo {
    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
            //取单例
            Student jack = Student.getInstance();
            oos.writeObject(jack);
            oos.close();

            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
            Student stu = (Student) ois.readObject();
            ois.close();
            System.out.println(stu);
           //看反序列化后是否是同一个对象
            System.out.println(jack == stu);

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

Demo输出结果为:

Student{age=23, name='jack'}
false

可以看出,反序列化后不是同一个对象。那要怎么解决这个问题呢?可以在单例类Student中加个私有方法readResolve()方法。

import java.io.*;

/**
 * <p>文件描述: 需要序列化的类需要实现Serializable接口</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午23:20
*/
public class Student implements Serializable{

    private Integer age;
    private String name;

    private static class Instance {
        private static Student jack = new Student(23, "jack");
    }

    private Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    public static Student getInstance() {
        return Instance.jack;
    }
    //增加readResolve方法,直接返回单例
    private Object readResolve() {
        return Instance.jack;
    }

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

}

Demo输出结果为:

Student{age=23, name='jack'}
true

可以看到,反序列化后的对象和序列化之前的对象是同一个对象。

无论是实现Serializable或者Externalizable接口的对象,反序列化的时候都会调用readResolve方法,因此这里的readResolve方法直接返回了单例,而不是返回反序列化后的对象,保证了单例的逻辑。

序列化ID

实现Serializable接口的类需要生成对应的序列化id(serialVersionUID)。如:

    private static final long serialVersionUID = -5881678404557689785L;

如果没有显示为类添加该字段(IDE可以设置成自动生成),java将会根据字节码文件动态生成序列化ID。每次重新编译生成的序列化ID都不一样。所以前面的Student例子都不是完美的,没有生成序列化ID。

如果显示为类添加该字段,那么不管编译多少次,只要没有修改该字段的值,该字段就不会变。

为何需要序列化ID?

如果一个类X在多个客户端使用,其中一个客户端A收到来自客户端B的类X对象x。客户端A通过网络收到对象x的二进制序列化文件后会进行反序列化操作。首先客户端A会取出对象x的序列化ID,并将该ID和客户端A本地类X Class对象的序列化ID进行比较,如果不相等那就会提示序列化失败,如果相等那就可以正常进行序列化。本人所在的公司现在使用的RPC框架、阿里开源的dubbo所使用的网络传输对象就是实现了Serializable接口。

父类和子类实现序列化的问题

  • 父类实现了序列化接口,子类自动拥有序列化功能,不需要显示的实现序列化接口
  • 父类没有实现序列化接口,子类实现了序列化接口。序列化子类对象时不会去序列化父类的对象。因为必须有父对象才有子对象,反序列化时会调用父类的无参默认构造函数,因此父类必须要有无参默认构造函数,反序列化后父类对象字段值都是其类型默认值(基本类型是0值,引用类型是null)

序列化多次相同对象的问题

如果序列化两个相同的对象,java做了特殊处理,看下例:

import java.io.*;

/**
 * <p>文件描述: Demo类</p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午23:40
*/
public class SerializableDemo {
    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
            Student jack = new Student(23, "jack");
            //第一次序列化对象
            oos.writeObject(jack);
            oos.flush();
            //第一次序列化对象后的文件大小
            System.out.println(new File("student.out").length());
            //第二次序列化同一个对象
            oos.writeObject(jack);
            //第二次序列化对象后的文件大小
            System.out.println(new File("student.out").length());
            oos.close();

            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
            //第一次反序列化对象
            Student stu1 = (Student) ois.readObject();
            //第二次反序列化对象
            Student stu2 = (Student) ois.readObject();
            ois.close();
            //比较是否同一个对象
            System.out.println(stu1 == stu2);

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

Demo输出结果为:

189
194
true

可以看到两次反序列化后的对象是同一个对象。

第一次序列化后的文件大小为189,第二次序列化后的文件大小为194,只增加了5字节,这显示是java做了优化导致的。两个相同对象当然不需要冗余存储,这增加的5字节是一些引用信息,这些引用指向同一个对象,反序列化后重建引用关系。

另外需要注意的是,如果在第二次序列化同一个对象之前,改变了这个对象某些字段的值,因为虚拟机发现之前已经将该对象序列化过了,这时只会存储写的引用,不会实际去写,所以第二次序列化之前对这些字段的改变实际上是没有效果的。

例如:

import java.io.*;

/**
 * <p>文件描述:Demo类 </p>
 *
 * @Author luanmousheng
 * @Date 17/7/1 下午23:54
*/
public class SerializableDemo {
    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
            Student jack = new Student(23, "jack");
            //第一次序列化对象
            oos.writeObject(jack);
            oos.flush();
            //第一次序列化对象后的文件大小
            System.out.println(new File("student.out").length());
            //第二次序列化之前将name值改为"lucy"
            jack.setName("lucy");
            //第二次序列化同一个对象
            oos.writeObject(jack);
            //第二次序列化对象后的文件大小
            System.out.println(new File("student.out").length());
            oos.close();

            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
            //第一次反序列化对象
            Student stu1 = (Student) ois.readObject();
            //第二次反序列化对象
            Student stu2 = (Student) ois.readObject();
            ois.close();
            //比较是否同一个对象
            System.out.println(stu1);
            System.out.println(stu2);
            System.out.println(stu1 == stu2);

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

输出结果为:

189
194
Student{age=23, name='jack'}
Student{age=23, name='jack'}
true

可以发现输出的两个对象值完全相同,第二次反序列化之前将值改为”lucy”是无效的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值