5

序列化,Java的实现方式

 2 years ago
source link: https://segmentfault.com/a/1190000040957611
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

当程序创建的对象,在程序终止后仍要存在,并在程序下次运行时重建对象,且拥有与程序上次运行时所拥有的信息相同。序列化可以将对象写入字节流,反序列化就是将字节流恢复为对象,如下图所示。

Java 中提供了一种通用序列化机制,实现将对象写出到输出流中,并在之后将其读回。

对象序列化

定义一个 Student 类,如下所示。

public class Student implements Serializable {
    private static final long serialVersionUID = -4496225960550340595L;
    private String name;
    private Integer age;
    private Double score;
    ...getter与setter...
    @Override
    public String toString() {
        return new StringJoiner(", ", Student.class.getSimpleName() + "[", "]")
                .add("name='" + name + "'")
                .add("age=" + age)
                .add("score=" + score)
                .toString();
    }
}

在程序中使用 Student 类可以创建实例来表示某一学生。

Student s = new Student();
s.setName("小赵");
s.setAge(24);
s.setScore(98.5);

但是,在程序结束后,该实例会被销毁,如果想在下次运行时拥有与上次运行时相同的信息,使用 ObjectOutputStream 实例将 Student 实例序列化保存到文件中。

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("xxx"));
out.writeObject(s);
out.close();

对象序列化才使用 writeObject/readObject 方法,基本类型需要使用 writeInt/readIntwriteDouble/readDouble 这样的方法,对象流类都实现了 DataInput/DataOutput 接口。

保存到文件中后,想要使用的话就可以通过创建 ObjectInputStream 实例并调用 readObject() 方法来获取。

ObjectInputStream in = new ObjectInputStream(new FileInputStream("xxx"));
Student saved = (Student) in.readObject();
in.close();

Serializable

Java 想要使一个类能被序列化,就需要像 Student 一样,实现 Serializable 接口。

public interface Serializable {
}

该接口没有任何方法需要实现,只是作为类能被序列化的标记。但是没有该接口的话,序列化会报 java.io.NotSerializableException 异常。源码中序列化如下所示。

if (obj instanceof String) {
    writeString((String) obj, unshared);
} else if (cl.isArray()) {
    writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
    writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {    // 判断是否实现了 Serializable 接口
    writeOrdinaryObject(obj, desc, unshared);
} else {
    if (extendedDebugInfo) {
        throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
    } else {
        throw new NotSerializableException(cl.getName());
    }
}

当然,实现 Serializable 接口的可序列的类,在序列化时,会默认将类中所有信息序列化,如果想要控制序列化对象,可以使用 transient 关键字标识需要关闭序列化的字段,如下所示。

public class Student implements Serializable {
    private static final long serialVersionUID = -4496225960550340595L;
    private String name;
    private transient Integer age;
    private Double score;
    ···
}
// 序列化前:Student[name='小赵', age=24, score=98.5]
// 反序列化后:Student[name='小赵', age=null, score=98.5]

这样使用 transient 进行标记,就可以在对象序列化时跳过。

使用 Serializable 的默认序列化会降低改变类实现的灵活性,增加 Bug 和安全漏洞的可抗性,测试也会增加负担等,因此想使用 Serializable 实现序列化时要考虑清除。

Externalizable

除了 Serializable 接口外,Java 还提供了继承 Serializable 接口的 Externalizable 接口,并且还要实现两个方法。

public class Student implements Externalizable {
    ...
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
    }
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    }
}

Java 提供的 Externalizable 接口可以控制对象序列化时,不想被其序列化的信息,和反序列时,不被反序列化的信息。

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

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {    // 反序列顺序要与序列化时的顺序一致,否则会报 java.lang.ClassCastException 异常
    name = (String) in.readObject();
    score = (Double) in.readObject();
}

这样就不会将 Student 中的 age 序列化了,对序列化过程进行控制。

readObjectwriteObject 方法是私有的,并且只能被序列化机制调用。与此不同的是,readExternalwriteExternal 方法是公共的。特别是,readExternal 还潜在地允许修改现有对象的状态。

serialVersionUID

在实现 Serializable 接口的可序列化的类里,都需要显示的增加下面这行代码。

private static final long serialVersionUID = -4496225960550340595L;

该静态数据域用于显示声明序列版本 UID,并在序列化机制中,通过判断 serialVersionUID 来验证版本一致性。因此,反序列时,只要 serialVersionUID 与本地相应实体类的 serialVersionUID 一致,就可以反序列化,实现兼容,否则会报 java.io.InvalidClassException 异常。因此,只要 serialVersionUID 不变,序列化就可以读入这个类的对象的不同版本。

如果被序列化的对象具有在当前版本中所有没有的数据域,反序列化时会忽略额外的数据;如果当前版本具有在被序列化的对象所没有的数据域,那么新添加的域将被设置成它们的默认值。

显式的声明 serialVersionUID 也会带来小小的性能好处。如果没有提供显式的序列版本 UID,编译器会选择一个摘要算法,并在运行时通过高开销的计算过程来产生一个序列版本 UID,只要这个类有改动,得到的 UID 也就会变化,到时对象输入流将会拒绝反序列具有不同序列版本 UID。因此,在可序列化的类中声明显式的 serialVersionUID

当你确定默认的序列化形式就满足了当前环境,还必须提供一个 readObject 方法添加验证或其他行为以保证约束关系和安全性。

private void readObject(ObjectInputStream in);
private void writeObject(ObjectOutputStream out);

之后,数据域就再也不会被自动序列化,取而代之的是调用这些方法。

如上所示,当反序列化时,如果学生分数不在 0~100 之间,就是错误的数值,可以保护性地编写 readObject() 方法,维护其约束条件。

public class Student implements Serializable {
    ...
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();    // 调用默认反序列化方法
        if (score < 0 || score > 100)
            throw new IllegalArgumentException("The score is between 0 and 100.");    // 判断如果学生成绩有问题,抛出异常,终止操作。
    }
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();    // 调用默认序列化方法
    }
}

枚举序列化

当目标对象唯一时,可使用枚举实现序列化,如下所示。

public enum Week {
    MONDAY,       // 星期一
    TUESDAY,      // 星期二
    WEDNESDAY,    // 星期三
    THURSDAY,     // 星期四
    FIRDAY,       // 星期五
    SATURDAY,     // 星期六
    SUNDAY;       // 星期日
}

为了保证枚举类型及其定义的枚举变量在 VM 中是唯一的,Java规定了枚举常量的序列化是通过ObjectOutputStream 将枚举的 name() 方法返回的值做序列化;反序列化时,通过 ObjectInputStream 从流中读取常量名称,然后调用 java.lang.Enum.valueOf() 方法获得反序列化常量,并将常量的枚举类型和收到的常量名称作为参数传递。

public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
    T result = enumType.enumConstantDirectory().get(name);
    if (result != null)
        return result;
    if (name == null)
        throw new NullPointerException("Name is null");
    throw new IllegalArgumentException(
        "No enum constant " + enumType.getCanonicalName() + "." + name);
}

编译器在序列化和反序列化时,会忽略枚举类型定义的任何类特定的 writeObjectreadObjectReadObjectNodeDatawriteReplacereadResolve方法。类似地,也会忽略任何 serialPersistentFieldsserialVersionUID 字段声明,所有枚举类型都具有规定的 serialVersionUID 0L。记录枚举类型的可序列化字段和数据是不必要的,因为发送的数据类型没有变化。

在序列化和反序列化时,如果目标对象是唯一的,那么你必须加倍当心,这通常会在实现单例和类型安全的枚举时发生。

但在枚举之前,是通过使用 static final 来表示枚举类型,如下所示。

public class Week {
    public static final Week MONDAY = new Week(1);
    public static final Week TUESDAY = new Week(2);
    public static final Week WEDNESDAY = new Week(3);
    public static final Week THURSDAY = new Week(4);
    public static final Week FIRDAY = new Week(5);
    public static final Week SATURDAY = new Week(6);
    public static final Week SUNDAY = new Week(7);
    private int value;
    private Week(int v) {
        this.value = v;
    }
}

但是,为这个类实现 Serializable 接口变成可序列化的类后,默认序列化机制就不再适用,任何 readObject 方法都会返回一个新键的实例。

public class Week implements Serializable {
    public static final Week MONDAY = new Week(1);
    ···
}

Week w = FIRDAY;
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("xxx/ioweek.txt"));
out.writeObject(w);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("xxx/ioweek.txt"));
Week s = (Week) in.readObject();
in.close();

反序列化获取的 Week 对象与序列化的 Week 对象比较 w == sfalse,新键的实例与该类初始化时创建的实例不同,说明即使构造器私有,序列化机制也可以创建新实例。

readResolve 特性允许用 readObject 创建的实例代替另一个实例。因此,在 Week 类中定义 readResolve 方法,如下所示。

private Object readResolve() throws ObjectStreamException {
    if (value == 1) return Week.MONDAY;
    if (value == 2) return Week.TUESDAY;
    if (value == 3) return Week.WEDNESDAY;
    if (value == 4) return Week.THURSDAY;
    if (value == 5) return Week.FIRDAY;
    if (value == 6) return Week.SATURDAY;
    if (value == 7) return Week.SUNDAY;
    throw new ObjectStreamException();
}

定义 readResolve 方法后,反序列化时新键的对象会通过调用 readResolve 方法返回的值称为 readObject 的返回值,因此 w == s 返回的就是 true

序列化实现克隆

可序列化的类可以使用序列化机制实现对象克隆。如下所示,为可序列化的 Student 类提供克隆方法。

public class Student implements Cloneable, Serializable {
    ...
    @Override
    public Object clone() throws CloneNotSupportedException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();    
        try {
            // save the object to a byte array
            try (
                    ObjectOutputStream out = new ObjectOutputStream(bout)
                    ) {

                out.writeObject(this);
            }
            // read a clone of the object from the byte array
            try (
                    ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray()))
            ) {
                return in.readObject();
            }

        } catch (IOException | ClassNotFoundException e) {
            CloneNotSupportedException ex = new CloneNotSupportedException();
            ex.initCause(e);
            throw ex;
        }
    }
}

如上所述,类想实现 clone 方法,需要实现 Cloneable 接口,之后调用该 clone 方法即可。使用 ByteArrayOutputStream 将数据保存到字节数组中,而不必将对象写出到文件中。

Student s = new Student();
s.setName("小赵");
s.setAge(24);
s.setScore(98.5);

Student sc = (Student) s.clone();

但这种方式也会比复制或克隆数据域的克隆方法要慢得多。当然,Java 序列化也是有风险的,最好是避免在程序中使用。

欢迎关注公众号「海人为记」,期待与你共同进步!

image


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK