关闭

Java网络编程由浅入深五 Java对象的序列化与反序列

标签: 网络编程java
290人阅读 评论(0) 收藏 举报
分类:

对象的序列化与反序列化

概念

对象的序列化:将Java对象转换为字节序列的过程称为对象的序列化。
对象的反序列化:把字节序列恢复为Java对象的过程称为对象的反序列化。

序列化的作用

1、把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中。
2、在网络上传送对象的字节序列。

实现序列化的方式

JDK类库中的序列化API,只有实现了Serializable或Externalizable接口的类对象才能被序列化,否则会出现java.io.NotSerializableException异常。
实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式。

实现了Serializable接口的类A,按照如下方式序列化及反序列化A对象:
1、ObjectOutputStream 采用默认的序列化方式,A的对象的非transient的实例变量进行序列化。
2、ObjectInputStream采用默认的反序列化方式,A对象的非transient的实例变量进行反序列化。

实现了Serializable接口的类A,并且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputStream out)方法,按照如下方式序列化及反序列化A对象:
1、ObjectOutputStream会调用A类的writeObject(ObjectOutputStream out)方法进行序列化。
2、ObjectInputStream会调用A类的readObject(ObjectInputStream in)方法进行反序列化。

如果A类实现了Externalizable接口,那么A类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法。按照如下方式进行序列化与反序列化:
1、ObjectOutputStream会调用A类的writeExternal(ObjectOutput out)方法进行序列化。
2、ObjectInputStream会调用A类的readExternal(ObjectInput in)方法进行反序列化。

实现Serialiable接口

通过上面的分析,ObjectOutputStream只能对实现了Serializable接口的类的对象进行序列化。

代码示例

新建一个Custom类实现Serializable 接口。并新建测试类,将序列化的数据保存到文件,然后再通过解析文件进行反序列化。

/**
 * Customer类
 *
 * @author 在路上的coder
 * @create 2017-03-06
 **/
public class Customer implements java.io.Serializable {


    private static final long serialVersionUID = -8763780870464864012L;

    private static int count;
    private static final int MAX_COUNT = 1000;
    private String name;
    private transient String password;

    static {
        System.out.println("调用了Customer类的静态代码块");
    }

    public Customer() {
        System.out.println("调用了Customer类的不带参数的构造方法");
        count++;
    }

    public Customer(String name, String password) {
        System.out.println("调用了Customer类的带参数的构造方法");
        this.name = name;
        this.password = password;
        count++;
    }

    public String toString() {
        return "count=" + count + " MAX_COUNT=" + MAX_COUNT + " name=" + name + " password=" + password;
    }
}

测试类

/**
 * 测试类
 *
 * @author 在路上的coder
 * @create 2017-03-07
 **/
public class SerTest {

    @Test
    public void writeFile() throws IOException {
        Customer customer = new Customer("张三", "12312");
        System.out.println("序列化之前:" + customer);

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("C:\\application\\JetBrains\\demo\\network\\src\\com\\knight\\chapt9\\objectFile.obj"));
        objectOutputStream.writeObject(customer);
        objectOutputStream.close();

    }

    @Test
    public void readFile() throws IOException, ClassNotFoundException {
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("C:\\application\\JetBrains\\demo\\network\\src\\com\\knight\\chapt9\\objectFile.obj"));
        Customer serCustomer = (Customer) inputStream.readObject();
        System.out.println("序列化之后:"+serCustomer);
        inputStream.close();
    }
}

执行writeFile()方法输出如下:
这里写图片描述

执行readFile() 方法输出如下:
这里写图片描述

结论:
从上面打印的结果可以看到,反序列化的时候,加载并初始化Customer类,在初始化时,

  • 先执行静态代码块。
  • 并将静态变量count的值初始化为0。
  • 反序列化时不会调用类的任何构造方法。
  • 被transient关键字修饰的没有被序列化。

序列化对象图

类与类之间可能存在关联关系。Customer与Order之间存在一对多的关系。
添加Order类的代码:

/**
 * 订单类
 * @author 在路上的coder
 * @create 2017-03-07
 **/
public class Order implements java.io.Serializable{

    private static final long serialVersionUID = 7027615442146635452L;

    private String orderId;//订单号
    private BigDecimal total;//订单金额

    public Order(String orderId,BigDecimal total){
        this.orderId = orderId;
        this.total = total;
    }

    @Override
    public String toString() {
        return "Order{" +
                "orderId='" + orderId + '\'' +
                ", total=" + total +
                '}';
    }
}

在序列化Customer类的时候Order类也会序列化。
如下图,按照默认方式序列化对象A时,将会序列化那些对象?
这里写图片描述
在实例A对象的时候,将会实例化B、C、D、E对象。

控制序列化的行为

实现方式:
通过在可序列化类中提供以下方法:

    private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {}

    private void writeObject(ObjectOutputStream outputStream) throws IOException {}

当ObjectOutputStream对一个Customer对象进行序列化时,如果Customer对象具有writeObject()方法,那么就会执行这一方法,否则就按默认方式序列化,反序列化也是如此。
下面通过两个例子来验证这一点,其中一个示例是,对敏感数据序列化时加密,反序列化时解密。

/**
 * Customer类
 *
 * @author 在路上的coder
 * @create 2017-03-06
 **/
public class Customer implements java.io.Serializable {


    private static final long serialVersionUID = -8763780870464864012L;

    private static int count;
    private static final int MAX_COUNT = 1000;
    private String name;
    private transient String password;


    static {
        System.out.println("调用了Customer类的静态代码块");
    }


    public Customer() {
        System.out.println("调用了Customer类的不带参数的构造方法");
        count++;
    }

    public Customer(String name, String password) {
        System.out.println("调用了Customer类的带参数的构造方法");
        this.name = name;
        this.password = password;
        count++;
    }

    public String toString() {
        return "count=" + count + " MAX_COUNT=" + MAX_COUNT +
                " name=" + name + " password=" + password;
    }

    /**
     * 加密数组
     *
     * @param buff
     * @return
     */
    private byte[] change(byte[] buff) {
        for (int i = 0; i < buff.length; i++) {
            int b = 0;
            for (int j = 0; j < 8; j++) {
                int bit = (buff[i] >> j & 1) == 0 ? 1 : 0;
                b += (1 << j) * bit;
            }
        }
        return buff;
    }

    /**
     * 反序列化执行方法
     *
     * @param inputStream
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
        System.out.println("开始执行反序列化方法");
        inputStream.defaultReadObject();
        byte[] bytes = (byte[]) inputStream.readObject();
        password = new String(change(bytes));
    }

    /**
     * 序列化执行方法
     *
     * @param outputStream
     * @throws IOException
     */
    private void writeObject(ObjectOutputStream outputStream) throws IOException {
        System.out.println("开始执行序列化方法");
        outputStream.defaultWriteObject();
        outputStream.writeObject(change(password.getBytes()));//序列化密码
    }
}

对象属性有效性校验示例,新建一个Student类,该类有一个age属性,该属性的值必须大于0;


/**
 * 测试序列化校验有效性
 * @author 在路上的coder
 * @create 2017-03-07
 **/
public class Student implements java.io.Serializable{
    private static final long serialVersionUID = 35245843321025025L;

    private int age;

    public Student(int age){
        if(age<0){
            throw new IllegalArgumentException("年龄必须大于零");
        }
        this.age = age;
    }

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

测试方法:

 @Test
    public void testValidate() throws IOException, ClassNotFoundException {
        Student student = new Student(25);
        System.out.println("序列化之前:"+student);

        ByteArrayOutputStream buf = new ByteArrayOutputStream();
        //将Custom4对象序列化到一个字节缓存中
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(buf);
        objectOutputStream.writeObject(student);

        byte[] byteArray = buf.toByteArray();

        for(int i=0;i<byteArray.length;i++){
            System.out.print(byteArray[i]+" ");
            if((i%10==0 && i!=0) || i==byteArray.length-1){
                System.out.println();
            }
        }

        //篡改序列化数据
        byteArray[byteArray.length-4] = -1;
        byteArray[byteArray.length-3] = -1;
        byteArray[byteArray.length-2] = -1;
        byteArray[byteArray.length-1] = -10;


        //从字节缓冲中反序列化Customer4对象
        ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(byteArray));
        Student s = (Student) inputStream.readObject();
        System.out.println("序列化之后:"+s);
    }

执行结果:
这里写图片描述
类的age属性已经被修改了,而且还无法校验。通过添加readObject()方法来实现校验。修改Student类如下:

/**
 * 测试序列化校验有效性
 * @author 在路上的coder
 * @create 2017-03-07
 **/
public class Student implements java.io.Serializable{
    private static final long serialVersionUID = 35245843321025025L;

    private int age;

    public Student(int age){
        if(age<0){
            throw new IllegalArgumentException("年龄必须大于零");
        }
        this.age = age;
    }

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

    private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
        inputStream.defaultReadObject();
        if(age<0){
            throw new IllegalArgumentException("年龄必须大于零");
        }
    }
}

再次执行测试方法,结果如下图:
这里写图片描述

readResolve()方法在单例类中的应用

单例类是指只有一个实例的类。无论采用默认方式,还是采用用户自定义的方式,反序列化都会创建一个新的对象。
代码如下:

/**
 * 单例模式
 * @author 在路上的coder
 * @create 2017-03-07
 **/
public class Singleton implements java.io.Serializable{
    private static final Singleton INSTANCE = new Singleton();

    private Singleton(){

    }

    public static Singleton getInstance(){
        return INSTANCE;
    }


    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Singleton singleton = Singleton.getInstance();
        ByteArrayOutputStream buff = new ByteArrayOutputStream();
        ObjectOutputStream outputStream = new ObjectOutputStream(buff);
        outputStream.writeObject(singleton);


        ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(buff.toByteArray()));
        Singleton newSingleton= (Singleton) inputStream.readObject();
        System.out.println("singleton==newSingleton:"+(singleton==newSingleton));
        System.out.println("singleton.equals(newSingleton):"+(singleton.equals(newSingleton)));
    }
}

执行结果如下:
这里写图片描述

由此可见,反序列化打破了单例类只能有一个实例的约定。为了避免这一问题,可以在 Singleton类中增加一个readResolve()方法。

    public Object readResolve(){
        return INSTANCE;
    }

执行结果如下:
这里写图片描述

执行流程如下:
如果一个类提供了readResolve()方法,那么在执行反序列化操作时,先按照默认方式或者自定义的方式进行反序列化,最后调用readResolve()方法,该方法返回的对象为反序列化的最终结果。

有readResolve()方法那自然就有writeReplace()方法,用来重新指定被序列化的对象。writeReplace()方法返回一个Object类型的对象,这个返回对象才是真正要被序列化的对象。

实现Externalizable接口

Externalizable接口继承自Serializable接口,如果一个类实现了Externalizable接口,那么将完全由这个类控制自身的序列化行为。Externalizable接口中申明了两个方法:

void readExternal(ObjectInput in)throws IOException, ClassNotFoundException;
void writeExternal(ObjectOutput out)throws IOException

writeExternal()负责序列化操作,readExternal()方法负责反序列化操作。在实现了Externalizable接口的类的对象进行反序列化时,会先调用类的不带参数的构造方法,这里有别于实现Serializable默认反序列化方式。

可序列化类的不同版本

凡是实现了Serializable接口的类都有一个表示序列化版本标识符的静态常量:

private static final long serialVersionUID = 35245843321025025L;

以上serialVersionUID 的取值是Java运行时环境根据类的内部细节自动生成的。如果不指定的话,如果对类的源码进行修改,再重新编译,新生成的文件的serialVersionUID的取值也会发生变化。
类的serialVersionUID的默认值依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的serialVersionUID,也有可能相同。为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列类中显示地定义serialVersionUID,为它赋予明确的值。显示定义serialVersionUID的用途:
1、在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID。
2、在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

Tips
是要serialVersionUID来控制序列化兼容性的能力非常有限,就算serialVersionUID相通,任然可能出现序列化不兼容的情况。因为序列化兼容性不仅取决于serialVersionUID,还取决于类的不同版本的实现细节和序列化细节。

总结

本文介绍了实现序列化的方式、控制序列化的行为、并使单例类实现真正的单例还有序列化后类的版本兼容问题。


欢迎关注微信公众号 在路上的coder 每天分享优秀的Java技术文章,还有学习视频分享!
扫描二维码关注:这里写图片描述

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    欢迎关注个人微信号
      欢迎关注微信账号:在路上的coder .每天一篇java相关或互联网相关的文章




    个人资料
    • 访问:796470次
    • 积分:2728
    • 等级:
    • 排名:第13821名
    • 原创:86篇
    • 转载:4篇
    • 译文:0篇
    • 评论:23条
    资源分享地址
    个人博客地址
    博客专栏
    最新评论