浅谈 Java 序列化(涉及部分源码分析)

序列化是什么?

用一句话来概括:序列化是用来保存对象状态的一种机制

什么是对象的状态?比如现在有一个类:

class A {
	int a;
	int b;

	A(int a, int b) {
		this.a = a;
		this.b = b;
	}
}

然后我们去声明了一个类A的实例:

A a = new A(1, 2);

现在这个对象中实例变量的值为1和2,这就是对象的状态。

对象的状态,其实就是目前该对象实例在堆中所存有的各种实例变量的值。

我们知道,对象在网络中是不能直接传递的,但是字节序列可以。
而序列化可以将这个对象状态转换为字节流,将其保存到磁盘文件亦或是通过网络传输。

序列化主要用来干什么?

  1. 实现持久性,比如将某个对象状态持久化到redis数据库中,需要的时候再取出来用。
    当这个对象被序列化时,意味着该对象的生存周期已经不再取决于程序的运行,你可以在任意时刻获取该对象某一时刻的状态,只要你把该对象序列化存储下来。
  2. 对于对象的传输,网络间对象状态的传输,典型的应用如RMI,利用Java序列化实现远程通信。

我为什么不能自己实现序列化?

答案当然是可以的,但是你仔细的想一想,如果一个类包含许多类,或者包含很多引用的对象,你难道要自己一点一点去深度遍历每个对象,并将它们一一序列化吗?

Java序列化可以递归保存对象引用的每个对象的数据,其一定是比你自己实现的更加高效快捷,而你要做的只是实现这个接口而已。

如何实现序列化?

实现 Serializable 或者 Externalizable 接口。
实际上 Externalizable 接口也实现了Serializable接口,所以可以说序列化就是实现 Serializable接口。

下面主要对Serializable进行介绍,对于Externalizable的实现相关源码不再说明。

Serializable接口

实现理论

一个类只有实现了Serializable接口,它的对象才是可序列化的,同时它的所有子类型都是可序列化的。

Serializable接口位于 java.io 包中,我们首先来看它的源码是如何写的:

public interface Serializable {}

什么都没有?没错,就是什么都没有。
那我实现这个接口有什么用?为什么实现这个接口就能实现序列化呢?
我们来看源码当中的注释,有一句话是这么写的:
(英文水平有限的可以去查Java API 的中文版文档:JDK 1.8 API 中文版

The serialization interface has no methods or fields and serves only to identify the semantics of being serializable.
(序列化接口没有方法或字段,仅用于标识可序列化的语义)

也就是说,这个接口仅仅起一个标识的作用,那不用代码去处理,到哪里去处理呢?
答案是显而易见的——JVM替我们做了这件事情。

我把这个标识理解为一种委托机制,就比如你女朋友想要买某样商品,于是把链接发给了你,这个链接其实就是一种标识,你看到之后立马明白,于是点开链接,下单,购买,拿包裹,最终将那样商品递到老婆大人面前,这个过程是你女朋友做的吗?并不是,她做的仅仅只是提供了一个标识,而实际的过程由你来完成。

实现了Serializable接口的类,实际上就是告诉JVM:这个类的对象可以被序列化,而具体的操作过程,如何去序列化,是JVM替我们去完成的,我们要做的事实上只有一件事:实现接口,之后就默认此类的对象已经可以被序列化储存。

另外,对于序列化具体的操作,源码注释中也有说明:

Classes that require special handling during the serialization and deserialization process must implement special methods with these exact signatures:
(在序列化和反序列化过程中需要特殊处理的类必须实现具有以下确切签名的特殊方法:)

private void writeObject(java.io.ObjectOutputStream out)
     throws IOException
private void readObject(java.io.ObjectInputStream in)
     throws IOException, ClassNotFoundException;
private void readObjectNoData()
     throws ObjectStreamException;
  1. 关于writeObject()方法:

The writeObject method is responsible for writing the state of the object for its particular class so that the corresponding readObject method can restore it.
(writeObject方法负责为其特定类写入对象的状态,以便相应的readObject方法可以恢复它。)

这个很好理解,也就是说序列化写入对象的状态所用的方法为writeObject(),而反序列化也就是恢复的方法是readObject()

The default mechanism for saving the Object’s fields can be invoked by calling out.defaultWriteObject.
(可以通过调用out.defaultWriteObject来调用保存对象字段的默认机制。)

这个我的理解是,如果你有对writeObject()方法的重写,就调用该方法,如果没有的话,则调用defaultWriteObject()方法来以默认的机制保存对象字段。

The method does not need to concern itself with the state belonging to its superclasses or subclasses. State is saved by writing the individual fields to the ObjectOutputStream using the writeObject method or by using the methods for primitive data types supported by DataOutput.
(该方法不需要关心属于其超类或子类的状态。状态是通过使用writeObject方法或使用DataOutput支持的原始数据类型的方法将各个字段写入ObjectOutputStream来保存的。)

也就是说,我们要实现序列化的话,只要构造一个ObjectOutputStream类,然后调用其writeObject()方法即可。

  1. 关于readObject()方法:

The readObject method is responsible for reading from the stream and restoring the classes fields. It may call in.defaultReadObject to invoke the default mechanism for restoring the object’s non-static and non-transient fields. The defaultReadObject method uses information in the stream to assign the fields of the object saved in the stream with the correspondingly named fields in the current object. This handles the case when the class has evolved to add new fields. The method does not need to concern itself with the state belonging to its superclasses or subclasses. State is saved by writing the individual fields to the ObjectOutputStream using the writeObject method or by using the methods for primitive data types supported by DataOutput.
(readObject方法负责从流中读取并恢复类字段。它可以调用in.defaultReadObject来调用恢复对象的非静态和非瞬态字段的默认机制。defaultReadObject方法使用流中的信息来分配保存在流中的对象的字段,并将相应的字段命名为当前对象。当类演化为添加新字段时,它处理这种情况。该方法不需要关心属于其超类或子类的状态。状态是通过使用writeObject方法或使用DataOutput支持的原始数据类型的方法将各个字段写入ObjectOutputStream来保存的。)

与前面writeObject()方法的介绍基本一样,不再赘述。

  1. 关于readObjectNoData()方法:

The readObjectNoData method is responsible for initializing the state of the object for its particular class in the event that the serialization stream does not list the given class as a superclass of the object being deserialized. This may occur in cases where the receiving party uses a different version of the deserialized instance’s class than the sending party, and the receiver’s version extends classes that are not extended by the sender’s version. This may also occur if the serialization stream has been tampered; hence, readObjectNoData is useful for initializing deserialized objects properly despite a “hostile” or incomplete source stream.
(如果序列化流未将给定类列为反序列化对象的超类,则readObjectNoData方法负责初始化其特定类的对象的状态。 这可能发生在接收方使用与发送方不同的反序列化实例的类的版本的情况下,并且接收者的版本扩展了不被发送者版本扩展的类。 如果序列化流已被篡改,也可能发生这种情况; 因此,尽管存在“敌对”或不完整的源流,readObjectNoData可用于正确初始化反序列化对象。)

这个怎么解释呢?其实大概就是说:
当序列化与反序列化版本不同时,发生反序列化的类的超类不同于序列化时类的超类、有敌意或接收不完整的流时,如果没有定义readObjectNoData方法,则类的字段就会初始化成他们的默认值,否则readObjectNoData会取代readObject的调用。

还有两个特殊的方法:writeReplace()readResolve()
writeReplace()的声明:

ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;

This writeReplace method is invoked by serialization if the method exists and it would be accessible from a method defined within the class of the object being serialized. Thus, the method can have private, protected and package-private access. Subclass access to this method follows java accessibility rules.
(如果writeReplace方法存在,并且可以从正在序列化的对象的类中定义的方法访问该方法,则通过序列化调用该方法。因此,该方法可以具有私有、受保护和包私有访问。这个方法的子类访问遵循java可访问性规则。)

如果实现该方法,那么在序列化时会先调用该方法将当前对象替换成另一个对象并写入流中(看你在方法中返回什么)
实现该方法后就不需要writeObject()了,该方法返回值(要是可序列化的对象)会自动写入输出流中。
要注意的一点是,如果调用了该方法,那么反序列化时即便重写readObject()方法也不会调用了,会自动反序列化成该方法实现的对象。

readResolve()声明与源码中的介绍跟writeReplace()一样。
其作用是:在readObject()之后调用,对已经反序列化后的对象进行修改,并将修改后的结果作为readObject()的返回结果。

serialVersionUID

什么是serialVersionUID
源码中对它的解释是这样的:

The serialization runtime associates with each serializable class a version number, called a serialVersionUID, which is used during deserialization to verify that the sender and receiver of a serialized object have loaded classes for that object that are compatible with respect to serialization.
(序列化运行时将每个可序列化的类与称为serialVersionUID的版本号相关联,该序列号在反序列化期间用于验证序列化对象的发送者和接收者是否已加载与该序列化兼容的对象的类。)

也就是说,serialVersionUID是一个用于对象版本控制的版本号
比如我序列化了一个类A的对象,然后我修改了这个类的字段,此时我想恢复以前被序列化的对象,这是不被允许的,因为修改后的新类生成的serialVersionUID与旧类是不同的。
Java序列化依赖serialVersionUID版本号来恢复序列化对象的状态。

If the receiver has loaded a class for the object that has a different serialVersionUID than that of the corresponding sender’s class, then deserialization will result in an InvalidClassException.
(如果接收方加载了对象的一个类,这个对象的serialVersionUID与对应的发送方的类不同,那么反序列化将导致InvalidClassException。)

如果强行反序列化,将导致InvalidClassException 异常。

A serializable class can declare its own serialVersionUID explicitly by declaring a field named “serialVersionUID” that must be static, final, and of type long:
(serializable类可以显式地声明自己的serialVersionUID,方法是声明一个名为"serialVersionUID"的字段,这个字段必须是静态的、final的、类型为long:)

这个是规定了serialVersionUID的限定符,一个serialVersionUID必须是静态的、final的、类型为long的,比如:

ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;

但是如果没有显式的声明serialVersionUID会怎么样呢?

If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value for that class based on various aspects of the class, as described in the Java™ Object Serialization Specification.
(如果可序列化类没有显式声明serialVersionUID,则序列化运行时将根据Java(TM)对象序列化规范中所述的类的各个方面计算该类的默认serialVersionUID值。)

可以看到,其会根据类的一些细节计算该类默认的serialVersionUID值,但是强烈建议所有可序列化的类都声明serialVersionUID,为什么呢?

since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, and can thus result in unexpected InvalidClassExceptions during deserialization.
(API 中文版翻译的不怎么好,就不放中文了)

大致意思就是:默认的serialVersionUID计算对类的细节十分敏感,而类的细节可能因为编译器实现的不同而不同,这很可能会导致版本号计算不同造成的反序列化的失败。

另外,还强烈建议将serialVersionUID设置成private的:

since such declarations apply only to the immediately declaring class–serialVersionUID fields are not useful as inherited members.
(因为这样的声明只适用于立即声明的类——serialVersionUID字段作为继承成员是没有用的。)

父类实现了Serializable接口,子类可以被序列化,但是子类继承下来的UID是没有作用的。

Array classes cannot declare an explicit serialVersionUID, so they always have the default computed value, but the requirement for matching serialVersionUID values is waived for array classes.
(数组类不能声明显式的serialVersionUID,因此它们总是使用默认的计算值,但对于数组类,则放弃了匹配serialVersionUID值的要求。)

也就是说,对于数组类是没有UID的要求的。

具体实例

下面我们通过例子实现将序列化的对象存储到文件,再反序列化出来。

  1. 因为对象序列化是基于字节的,所以首先要创建OutputStream(输出流)对象,然后将其封装在一个ObjectOutputStream对象内。
    这里我们因为要存储到文件,所以构建一个文件输出流:
FileOutputStream fos = new FileOutputStream("W:\\people.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
  1. 调用writeObject()方法,即可将对象序列化,并将其发送给OutputStream

我们来看一下完整实例,这里我构建了一个People实体类:

@Data
public class People implements Serializable {

    /**
     * 定义唯一序列化号
     */
    private static final long serialVersionUID = 1L;

    /**
     * 名称
     */
    private String name;

    /**
     * 年龄
     */
    private Integer age;

    /**
     * 所在大学(若没有则此项为NULL)
     */
    private University university;

}

用了Lombok中的@Data注释来简化代码(自动添加getter和setter并重写toString)
为了验证对持有的引用对象的序列化,在People中加入了University类:

@Data
public class University implements Serializable {

	/**
     * 定义唯一序列化号
     */
    private static final long serialVersionUID = 1L;

    /**
     * 大学名称
     */
    private String name;

    /**
     * 所在城市
     */
    private String locale;
}

下面为People的实例数据赋值并将其序列化:

public class SerializeTest {
    public static void main(String[] args) throws Exception {
        University university = new University();
        university.setName("XX大学");
        university.setLocale("北京");

        People people = new People();
        people.setName("张三");
        people.setAge(21);
        people.setUniversity(university);

        System.out.println("Before Serialize : " + people);

        FileOutputStream fos = new FileOutputStream("W:\\people.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(people);
        oos.flush();
        oos.close();
        fos.close();
    }
}/* Output
Before Serialize : People(name=张三, age=21, university=University(name=XX大学, locale=北京))
*/

之后对其反序列化,从文件中将对象状态拿出:

public class SerializeTest {
    public static void main(String[] args) throws Exception {
        People people = null;

        FileInputStream fis = new FileInputStream("W:\\people.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);
        people = (People)ois.readObject();

        System.out.println(people);

        ois.close();
        fis.close();

    }
}/* Output
People(name=张三, age=21, university=University(name=XX大学, locale=北京))
*/

从打印结果中我们可以看到,People对象状态完全恢复,包括其持有的University对象。

将某字段设为不可序列化

transient关键字

我们知道序列化可以保存一个对象当前的状态,但是如果对象中某个实例变量并不想被保存到状态里,有没有什么办法达成这一点呢?
这就是transient关键字的作用,当你将某对象序列化时,对象中加上transient的字段并不会被持久化,也就是说序列化保存的对象状态是不包含该字段的

继续用上面那个例子,我们将age设为transient

@Data
public class People implements Serializable {

    /**
     * 定义唯一序列化号
     */
    private static final long serialVersionUID = 1L;

    /**
     * 名称
     */
    private String name;

    /**
     * 年龄
     */
    private Integer age;

    /**
     * 所在大学(若没有则此项为NULL)
     */
    private University university;

}

用上面相同的程序,对其序列化后进行反序列化:

public class SerializeTest {
    public static void main(String[] args) throws Exception {
        University university = new University();
        university.setName("XX大学");
        university.setLocale("北京");

        People people = new People();
        people.setName("张三");
        people.setAge(21);
        people.setUniversity(university);

        System.out.println("Before Serialize : " + people);

        FileOutputStream fos = new FileOutputStream("W:\\people.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(people);
        oos.flush();
        oos.close();
        fos.close();

        People p = null;

        FileInputStream fis = new FileInputStream("W:\\people.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);
        p = (People)ois.readObject();

        System.out.println("After: " + p);

        ois.close();
        fis.close();

    }
}/* Output
Before Serialize : People(name=张三, age=21, university=University(name=XX大学, locale=北京))
After: People(name=张三, age=null, university=University(name=XX大学, locale=北京))
*/

可以看到,age字段并没有被保存进去,取出来之后被赋予了默认的null值。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值