对象序列化

本文 是学习《深入理解Java7》 的学习笔记

在程序的运行过程中,活动对象的状态保存在内存中。内存中的数据是非持久化的。当虚拟机终止时,内存中对象的信息就会丢失。能够持久化保存对象数据的方式有很多。典型的方式是使用文件系统或数据库。持久化对象时通常涉及自定义存储格式。使用文件存储时可以基于标准的文件格式,如XML、JSON和CSV等,也可以使用自定义的文本或二进制格式。使用数据库存储时需要定义数据库的表结构。定义了存储格式之后,在保存和读取操作时,需要在活动对象的内部状态和存储格式之间互相转换。
从简化实现的角度出发,可以使用Java语言内建的对象持久化方式,即对象序列化(object serialization)机制。对象序列化用来在虚拟机中的活动对象和字节流之间进行转换。

序列化机制通常包括两个过程:

  • 序列化(serialization),把活动对象的内部状态转换成一个字节流,保存操作。
  • 反序列化(deserialization),从一个字节流中得到可以直接使用的Java对象。读取操作。

默认的对象序列化

待序列化的Java类只需实现java.io.Serializable接口即可启用这个功能。Serializable仅是一个标记接口,并不包含任何需要实现的具体方法。实现该接口的目的是声明该Java类的对象是可以被序列化的。对于支持序列化的Java类的对象,可以使用java.io.ObjectOuputStream类和java.io.ObjectInputStream类的对象来完成Java对象与字节流之间的相互转换。
- 待实例化的User类

package serializable;

import java.io.ObjectStreamField;
import java.io.Serializable;

/**
 * writeObject方法的Java对象并没有实现Serializable接口,
 * 那么writeObject方法会抛出java.io.NotSerializableException异常。
 * 在默认的序列化实现中,Java对象中的非静态和非瞬时域都会被自动包括进来,
 *  而与域的可见性声明没有关系。这可能会导致某些不应该出现的域被包含在序
 * 列化之后的字节流中 ,比如密码等隐私信息对应的域,进而造成隐私信息的 泄露,
 * 成为程序中的安全隐患。针对这种情况,一种解决办法是把不希望被序
 * 列化的域声明为瞬时的 ,即使用transient关键词。另外一种做法是添加一 个
 * serialPersistentFields域来声明序列化时要包含的域。
 * private static final ObjectStreamField[] serialPersistentFields
 *  = { new ObjectStreamField("name", String.class) };
 */
public class User implements Serializable {
    private String name;
    private transient String email;
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return this.name;
    }

    public String getEmail() {
        return this.email;
    }
}
  • 写入Java对象的内容到文件中
package serializable;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamField;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 * 写入Java对象的内容到文件中
 * ObjectOuputStream类是一个Java  I/O库中标准的过滤输出流的实现。
 * 在创建ObjectOuputStream类的对象时 需要提供另外一个OutputStream
 * 类的对象作为实际的输出目的 。
 * 在ObjectOuputStream类中包含一系列以“write”作为名称前缀的方法用于
 * 写入基本类型的值和对象到输出流中 ,其中writeObject方法用于把一个
 * Java对象写入到输出流中。
 */
public class WriteUser {
    private static final ObjectStreamField[] serialPersistentFields = { new ObjectStreamField(
            "name", String.class) };
    public void write(User user) throws IOException {
        Path path = Paths.get("user.bin");
        try (ObjectOutputStream output = new ObjectOutputStream(
                Files.newOutputStream(path))) {
            output.writeObject(user);
        }
    }
    public static void main(String[] args) throws IOException {
        WriteUser writeUser = new WriteUser();
        User user = new User("Alex", "alex@example.org");
        writeUser.write(user);
    }
}
  • 从文件中读取Java对象
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 * 对象序列化之后得到的字节流可以通过不同的方式进行分发,比如文件复制或网络传输。
 * 在得到字节流之后,可以通过反序列化方式从字节流中得到Java对象。
 * 反序列化时使用的是与ObjectOuputStream类相对应的ObjectInputStream类。
 * 在创建ObjectInputStream类的对象时需要提供一个InputStream类的对象作为参数。
 * 该InputStream类的对象用于读取包含序列化操作结果的字节流。
 * ObjectInputStream类中包含一系列以“read”作为名称前缀的方法来从输入流中读取其中
 * 包含的基本类型数据 和对象其中readObject方法用于读取一个Java对象。
 * 读取到的Java对象中实例域的值由字节流中保存的值来确定。
 */
public class ReadUser {
    public User readUser() throws ClassNotFoundException, IOException {
        Path path = Paths.get("user.bin");
        try (ObjectInput input = new ObjectInputStream(
                Files.newInputStream(path))) {
            User user = (User) input.readObject();
            return user;
        }
    }
    public static void main(String[] args) throws ClassNotFoundException,
            IOException {
        ReadUser readUser = new ReadUser();
        User user = readUser.readUser();
        System.out.println("user.getName() " + user.getName());
        System.out.println("user.getEmail() " + user.getEmail());
    }
}

自定义对象序列化

默认的对象序列化机制虽然使用简单,但是存在的问题也比较多。其中最重要的问题是默认的序列化机制依赖于Java类的内部实现,而不是公开接口。随着程序的版本更新,公开接口基本上不会发生变化,而内部的实现可能发生很多变化。内部实现的变化会导致旧版本的Java对象序列化之后的字节流无法被重新转换成新版本的Java对象。这与通常的“面向接口编程”的实践方式是相背离的。
为了解决版本更新带来的序列化格式不兼容的问题,需要为Java类定义自己的序列化格式,而不是简单地使用默认格式。
通过在Java类中添加特定的方法来编写自定义的序列化实现逻辑。自定义的序列化逻辑由两个配对的方法来实现,分别是writeObject和readObject方法。

package serializable;

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

public class NewUser implements Serializable {
    /**
     * 可以在writeObject方法和readObject方法中添加比较复杂的逻辑,
     *  包括修改域的值或添加额外的数据等。
     */
    private static final long serialVersionUID = 1L;
    /**
     * 如果在自定义的writeObject方法和readObject方法中对某个域的数据进行了处理,
     * 一般把该域声明为transient,
     * 这样defaultWriteObject方法和defaultReadObject方法就不会处理这个域,
     * 避免默认实现带来的不兼容性问题。
     * 使用该ObjectInputStream类的对象来读取流中的内容,并初始化对象中的
     * 对应实例域。在readObject方法中一般先使用defaultReadObject方法
     */
    private transient int age;

    public NewUser(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    private void writeObject(ObjectOutputStream output) throws IOException {
        output.defaultWriteObject();
        output.writeInt(getAge());
    }
//  在writeObject方法中一般先使用defaultWriteObject方法来执行默认的写入操作,
//  即写入非静态和非瞬时的域。这样可以提高代码的灵活性。
    private void readObject(ObjectInputStream input)
            throws ClassNotFoundException, IOException {
        input.defaultReadObject();
        this.age = input.readInt();
        this.age = age;
    }

}

对象替换

  • 需要替换的类
package serializable;

import java.io.Serializable;

/**
 * 对象替换
 * 在进行反序列化操作时,会创建出新的Java对象。从某种意义上来说,反序列化的作用相当于一个构造方法。
 * 有些Java类对于其对象实例有自己的管理逻辑,
 * 例如使用单例模式时
 * ,要求某个类在虚拟机中只有一个实例。对于这样的Java类,在反序列化时,需要对得到的对象进行处理,
 * 以保证序列化机制不会破坏Java类本身的约束
 * ,否则,每进行一次反序列化操作,都会创建一个新的对象,就破坏了单例模式要求的约束条件。
 * 在其他情况下,可能需要在序列化时使用另外一个对象来代替当前对象,
 * 原因可能是当前对象中包含了一些不需要被序列化的域,比如这些域是从另外一个域派生而来的。
 * 对于这种情况,
 * 也可以通过把域声明为transient或使用自定义的writeObject方法和readObject方法来实现
 * 。但是如果这样的逻辑比较复杂,可以考虑封装在一个Java类中
 * 。另外还可以隐藏实际的类层次结构。比如类A中的某个域引用了类B,在正常的序列化过程中,
 * 类A和类B都会出现在序列化之后的字节流中
 * ,如果希望在字节流中隐藏类B,可以用另外一个类的对象来代替类B的对象。
 * 在序列化时进行的对象替换操作由一组对应的writeReplace方法和readResolve方法来完成
 * 。在writeReplace方法中可以根据当前对象创建一个新的对象作为替代
 * 。这个替代对象被写入到ObjectOutputStream类的对象中。同样的,在readResolve方法中
 * ,可以对读取出来的对象进行转换,把转换的结果作为反序列化的结果返回。
 */

public class Order implements Serializable {
    private User user;
    private String id;

    public Order(String id, User user) {
        this.id = id;
        this.user = user;
    }

    public String getId() {
        return id;
    }

    private Object writeReplace() {
        return new OrderTo(this);
    }

}
  • 替换对象的Java类
/**
 * OrderTO类本身使用了默认的序列化格式,只包含OrderTO类中表示订单编号的orderId域的内容。
 *  在调用readResolve方法时,基本的反序列化操作已经完成 ,orderId域已经被初始化为正确的值。
 *  在readResolve方法中通过相关的查找逻辑根据域orderId的值得到对应的Order类的对象 。
 *  把Order类的对象作为readResolve方法的返回值,即反序列化的最终结果。对调用者来说,
 * 替换对象的存在是透明的。
  */
public class OrderTo implements Serializable {
    private String orderId;

    public OrderTo(Order order) {
        this.orderId = order.getId();
    }

    private Object readResolve() throws ObjectStreamException {
        // 替换逻辑
        return null;
    }
}

版本更新

在使用ObjectInputStream类的对象读取一个旧版本的Java对象的序列化结果时,会尝试在当前虚拟机中查找其Java类的定义。如果找不到类的定义,就尝试加载该Java类,被加载的有可能是新版本的Java类。需要一种方式来判断旧版本的序列化内容是否与当前的Java类兼容。如果不兼容,那么读取操作会直接失败。这种兼容性的判断是通过Java类中的全局唯一标识符serialVersionUID来实现的。当Java类实现了Serializable接口时,需要声明该Java类唯一的序列化版本号。这个版本号会被包括在序列化后的字节流中。在进行读取时,会比较从字节流中得到的版本号与对应Java类中声明的版本号是否一致,以确定两者是否兼容。只有在版本一致时,序列化操作才能完成。

安全性

package serializable;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectInputValidation;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Date;

/**
 * 安全性 1.第一个方面是根据信息隐藏的原则,尽可能地减少序列化结果中包含的信息。 所包含的信息越少,
 * 泄露之后造成的影响就越小。通过自定义的序列化格式和对象替换可以实现这一点。
 * 第二个方面是对包含序列化结果的字节流进行保护。措施主要是加密和解密。可以对序列化
 * 后的字节流的整体使用各种加密算法进行处理。在反序列化之前再使用相应的解密算法进行
 * 处理即可。加密和解密也可以在序列化的过程中进行。在Java类的writeObject方法中,可
 * 以在写入域的值之前,先进行加密操作。在readObject方法中,在读取域的值之后,先进行
 * 解密操作,再赋值给对象中的域。这两种方式可以结合起来形成复杂的加密方案。
 * 3.第三个方面是在从字节流中进行反序列化操作时进行数据完整性验证。在使用ObjectInputStream类的
 * 对象进行读取操作时
 * ,输入字节流的内容有可能已经被篡改  ,因此在Java类的readObject方法中需要添加相应的验证代码来检
 * 查完整性是否被破坏,比如验证域的值是否为null,
 * 以及域的值是否在合理的范围之内等。在readObject方法中的处理适合于对单个Java类的对象进行验证。
 * 要对一个完整的
 * 对象图进行验证,可以通过ObjectInputStream类的registerValidation方法添加
 * java * .io.ObjectInputValidation接口的实现对象,进而添加完整的对象验证逻辑。通常的做法是
 * 在readObject方法中处理
 * 完所有域之后, * 再添加一个ObjectInputValidation接口的实现对象来进行完整的验证。验证通常在
 * 引用关系根节点的Java类的readObject
 * 方法中进行。
 * 
 * * 
 */
public class NewUser2 implements Serializable {
    /**
     * 可以在writeObject方法和readObject方法中添加比较复杂的逻辑, 包括修改域的值或添加额外的数据等。
     */
    private static final long serialVersionUID = 1L;
    /**
     * 如果在自定义的writeObject方法和readObject方法中对某个域的数据进行了处理,一般把该域声明为transient,
     * 这样defaultWriteObject方法和defaultReadObject方法就不会处理这个域,避免默认实现带来的不兼容性问题。
     */
    private transient int age;
    private transient Date birthDate;

    public NewUser2(Date birthDate) {
        this.birthDate = birthDate;
    }

    public int getAge() {
        return dateToAge(birthDate);
    }

    private int dateToAge(Date birthDate2) {
        // //日期转换成年龄
        return 0;
    }

    private void writeObject(ObjectOutputStream output) throws IOException {
        output.defaultWriteObject();
        int age = dateToAge(birthDate);
        output.writeInt(getAge());
    }

    private void readObject(ObjectInputStream input)
            throws ClassNotFoundException, IOException {
        input.defaultReadObject();
        this.age = input.readInt();
        this.birthDate = ageToDate(age);
        input.registerValidation(new UserValidator(this), 0);
    }

    private static class UserValidator implements ObjectInputValidation {
        private NewUser2 user;

        public UserValidator(NewUser2 user) {
            this.user = user;
        }

        @Override
        public void validateObject() throws InvalidObjectException {
            // TODO Auto-generated method stub
            if (user.getAge() < 0) {
                throw new InvalidObjectException("非法的年齡值");
            }
        }

    }

    private Date ageToDate(int age2) {
        // 年龄转换为日期
        return null;
    }

}

使用Externalizable接口

package serializable;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

/**
 * 实现Externalizable接口的Java类必须具备一个不带参数的公开构造方法。在反序列化的过程中,
 * 如果对象的Java类实现了Externalizable接口,则先调用不带参数的公开构造方法得到一个新的对象,
 * 再在此对象上调用readExternal方法。
 * Java类只需要简单地实现标记接口Serializable就可以使用Java平台提供的默认序列化机制。
 * 如果希望进行自定义的序列化处理,那么可以在Java类中添加writeObject方法和readObject方法。
 * 虽然可以进行自定义,但是完整的序列化过程仍然是由Java平台来控制的,无法修改序列化之后的二进制格式。
 * 如果希望对序列化的过程进行完全的控制,那么可以实现java.io.Externalizable接口。
 * Externalizable接口继承自Serializable接口,包含writeExternal和readExternal两个方法。
 * 在使用ObjectOutputStream类的对象写入Java对象时,如果该对象的Java类实现了Externalizable接口,
 * 则writeExternal方法会被调用;在使用ObjectInputStream类的对象读取Java对象时,
 * 如果实现了Externalizable接口,则readExternal方法会被调用。
 * 方法writeExternal和readExternal的作用类似于自定义序列化操作时使用的writeObject和
 * readObject方法,只不过这两个方法是在Externalizable接口中显式声明的,
 * 更容易被开发人员所理解。
 * 
 * 
 * 
 */
public class ExternalizableUser implements Externalizable {
    private String name;
    private String email;

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public ExternalizableUser(String name, String email) {
        this.name = name;
        this.email = email;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // TODO Auto-generated method stub
        out.writeUTF(getName());
        out.writeUTF(getEmail());
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        name = in.readUTF();
        email = in.readUTF();
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值