何为序列化,反序列化

将Java对象序列化为二进制形式->序列化
将二进制形式数据在内存中重建为java对象->反序列化

二进制中包含了当前实例的类的元数据,以及存储的数据等。

Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。也就是将Java对象序列化为二进制形式。

目的:

  • 网络传输
  • 持久化 (文件,DB等)

将序列化对象写入文件之后,可以从文件或网络中中读取出来,并且对它进行反序列化,也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。

如何序列化,反序列化

类能够序列化和反序列化,
1.类必须实现Serializable接口。
2.需要序列化的属性必须实现Serializable接口(不需要的属性可以使用transient关键字修饰)
3.第一个未实现Serializable接口的父类必须有无参构造器

Java中的内置序列化机制需要使用ObjectOutputStream完成序列化。
使用ObjectInputStream完成反序列化。

ObjectOutputStream

ObjectOutputStream能够让你把对象写入到输出流中,而不需要每次写入一个字节。你可以把OutputStream包装到ObjectOutputStream中,然后就可以把对象写入到该输出流中了。代码如下:

// Serialize today's date to a file.
    FileOutputStream f = new FileOutputStream("tmp");
    ObjectOutput s = new ObjectOutputStream(f);
    s.writeObject("Today");
    s.writeObject(new Date());
    s.flush();

ObjectInputStream

ObjectInputStream能够让你从输入流中读取Java对象,而不需要每次读取一个字节。你可以把InputStream包装到ObjectInputStream中,然后就可以从中读取对象了。代码如下:

// Deserialize a string and date from a file.
    FileInputStream in = new FileInputStream("tmp");
    ObjectInputStream s = new ObjectInputStream(in);
    String today = (String)s.readObject();
    Date date = (Date)s.readObject();

在这个例子中,写入和读取的顺序必须一致,数据类型也必须一致。

注意:可序列化的字段有两种方式确定
1.默认的:序列化不会处理静态和transient的属性。
2.使用serialPersistentFields

如何定制序列化的过程

1.使用默认的readObject/writeObject方法
方法签名:

private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException{
        ois.defaultReadObject();
        //其他read代码
    }
    private void writeObject(ObjectOutputStream oos) throws IOException,ClassNotFoundException {
        oos.defaultWriteObject();
        //其他write代码
    }

序列化时ObjectOutputStream调用writeObject方法,反序列化时ObjectInputStream调用readObject方法。

注意:
- 必须调用defaultWriteObject()和defaultReadObject()方法
- 写和读的顺序要一致,写了一个int,读的时候第一个也是读int.
- 序列化、反序列化过程中没有调用任何构造器(类及其父类均实现了Serializable接口时,如父类未实现Serializable,父类里的属性丢失,无法从流中反序列化)

2.使用Externalizable接口
方法签名

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

    }

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

    }

Externalizable是Serializable接口的子接口。
序列化、反序列化过程中调用了默认构造器

第一种方式可以部分定制序列化过程,序列化、反序列化的过程部分由Java控制,部分由程序员自己控制。
第二种方式则是完全定制序列化过程,程序员控制序列化、反序列化的过程.

序列化,反序列化在Java中是如何发生的

序列化:ObjectOutputStream开始向文件中写数据->反射调用writeObject方法并把自己传入->写完后转化为byte数组->写入文件

反序列化:读取文件->反射调用Object(如果某个父类没有实现Serializable接口,则调用该类的默认无参构造器(父类里的属性丢失))的构造器获取实例->设置
类型信息及其他实例化相关的元数据->初始化静态属性值->反射调用readObject方法->完成反序列化。

Externalizable 与 Serializable 的区别

Serializable

Externalizable

标志接口,无必须实现的方法

必须实现writeExternal() 和 readExternal()

JVM负责序列化实例,数据和数据类型等

程序员控制实例化及数据处理过程

默认不调用构造器

默认必须有无参构造器

修改类结构容易导致序列化失败,分析比较困难

分析和修改类容易

性能不好

性能受程序员实现限制,一般会比较好

类结构的兼容性

在序列化和反序列化的两端,java的类结构可能会有变动,有的变动会导致反序列化失败或引起业务系统的不兼容,称为非兼容的改动,
有的变动不会导致反序列化失败或业务系统的不兼容,称为兼容的改动。
注意:以下兼容性是在类实现了Serializable接口并定义了固定的serialVersionUID的情况下,如果没有实现该接口则由于serialVersionUID不同的原因,部分兼容的改动也会序列化失败。

兼容的改动

  • 修改属性的修饰符:public, package, protected, private 不影响序列化
  • 新增属性:新增的属性被设置默认值,可以通过readObject设置特定值。
  • 将static属性改为非static,将transient属性改为非transient:类似于新增属性,被设置为默认值。
  • 添加删除类:在类结构中添加的类对应的属性被设置为默认值。删除类,则类对应的属性被丢弃,但是引用到的类会被创建,在随后的垃圾回收中被回收掉。
  • 某个类中添加实现Serializable接口:同添加类一样,该的属性被设置为默认值。
  • 添加删除writeObject/readObject方法:添加时,方法会被调用,则可以对额外属性做序列化操作。删除时,额外添加的属性被丢弃。

不兼容的改动

  • 删除属性:删除属性会导致旧版本的属性被设置为默认值,引起业务逻辑的改动。
  • 类结构中提升或降低某个类的位置:不会允许,可能导致属性顺序混乱。
  • 修改基础类型的属性(primitive field)的类型:由于类型不匹配导致序列化失败。引用类型会最终递归到基础类型。
  • 修改writeObject/readObject方法的实现,使得某些属性不再使用默认序列化机制:即将交给jvm序列化的属性修改为直接代码处理了。类似于删除属性。writeObject/readObject对属性的处理应当保持一致。
  • 将非static属性改为static,将非transient属性改为transient:类似于删除属性。
    以下四种会导致数据的不兼容。
  • 修改类从实现Serializable接口改为实现Externalizable接口或者相反
  • 删除Serializable或Externalizable接口
  • 修改枚举类型为非枚举或者相反
  • 添加writeReplace或readResolve方法

需要注意的一些问题

serialVersionUID

private static final long serialVersionUID = -4116638233879810430L;

serialVersionUID是序列化的版本,用来在反序列化时,确保发送者和接收者加载的类是否兼容。其值可自定义。如果没有显示定义serialVersionUID属性,
则Java根据类定义使用SHA-1算法得出的哈希值替代。
serialVersionUID不一致时,报错InvalidCastException.
使用serialVersionUID可以在以上发生的兼容或不兼容的改动时,强制客户端更新。

transient 关键字 与 static属性

Java的序列化不考虑transient修饰的属性及static属性。
transient关键字一般用于:

  1. 某些属性无法被序列化
  2. 某些属性没有必要被序列化
  3. 某些属性序列化后会破坏业务逻辑,比如某个单例的属性
  4. 某些属性是由业务逻辑或者根据其他属性生成的
  5. 由于安全原因,不能序列化某些属性

父类的序列化

父类对象的序列化,需要让父类也实现Serializable 接口。
如果父类不实现,就需要有默认的无参的构造函数。
在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,
反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。
因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。
如果你考虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,
如 int 型的默认是 0,string 型的默认是 null。

readObjectNoData、writeReplace、readResolve

这三个方法都是Serializable接口的除readObject,writeObject方法之外的接口方法。

ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
    ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
       private void readObjectNoData()
           throws ObjectStreamException;

readObjectNoData是1.4新增的,目的在于发生:

  1. 序列化版本不兼容
  2. 输入流被篡改或者损坏
    时,给对象提供一个合理的值。

写入时替换对象:writeReplace

如果实现了writeReplace方法后,那么在序列化时会先调用writeReplace方法将当前对象替换成另一个对象(该方法会返回替换后的对象)并将其写入流中.
方法执行顺序:writeReplace -> writeObject/writeExternal
导致的结果:所有当前类型的实例都被替换了。假如一个类的100个不同的实例(某些属性的值完全不同)被序列化了,
反序列化时你得到了100个不同的实例,但是所有的属性可能都是一样的(取决于你的writeReplace方法的具体实现)。

读取时替换对象:readResolve

如果实现了readResolve方法后,那么在序列化时会最后调用writeReplace方法将当前对象替换成另一个对象。
方法执行顺序:readObject/readExternal -> readResolve
导致的结果:所有当前类型的实例都被替换了。

可以被用于单例模式,确保实例序列化后的唯一性。

默认实例化时,各方法的总结

功能点

readObject

writeObject

readExternal

writeExternal

readResolve

writeReplace

目的

读取定制内容

写入定制内容

读取完全定制的内容,类的实例化自己控制

写入完全定制的内容,写入当前对象的那些属性及写入顺序等自定义

覆盖读取的内容

覆盖写入的内容

是否使用默认序列化机制





是,但readObject/readExternal读到的内容被丢弃

是,但写入流的原类的实例都被丢弃

调用顺序

readObject -> readResolve

writeReplace -> writeObject

readExternal -> readResolve

writeReplace -> writeExternal

readObject/readExternal -> readResolve

writeReplace -> writeObject/writeExternal

是否成对

是,必须与writeObject一起添加


是必须与writeExternal一起添加

否,可独立存在

否,可独立存在

各个方法的目的不同,理解了目的,其他就好理解了。

序列化,反序列化与深克隆(deep clone)

java的克隆是属性到属性的克隆。

  • 如果属性是基础数据类型,新的属性对象会被创建。
  • 如果属性是引用类型,则只有引用被复制,引用指向的对象是同一个。意味着克隆对象的改动会影响被克隆对象。

java实现克隆需要两步。

  1. 实现clonable接口
  2. override Object对象的clone方法

java默认的克隆方式的浅克隆:即只克隆基础数据类型,引用类型只复制了引用。
深克隆:克隆所有的数据,使得对克隆对象的任何操作不会影响到原被克隆对象。
深克隆的方式可使用以下几种方式:

  1. 使用该类型参数构造器
public class Data {
        private Integer x;
        private Object y;

        public Data(Data point){
            this.x = point.x;
            this.y = point.y;
        }
    }
  1. 使用序列化、反序列化
public static  <T> T deepCopy(T Object) throws IOException, ClassNotFoundException {
            //Serialization of object
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);
            System.out.println("begin w");
            out.writeObject(Object);
            System.out.println("end w");

            //De-serialization of object
            ByteArrayInputStream bis = new   ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream in = new ObjectInputStream(bis);
            T copied = (T) in.readObject();
            System.out.println("end r");
            return copied;
        }

使用ByteArrayInputStream和ByteArrayOutputStream在内存中完成克隆

  1. 使用Apache commons工具
SomeObject cloned = org.apache.commons.lang.SerializationUtils.clone(someObject);

以上三种方式的性能未测试。

反序列化破坏单例

反序列化默认没有调用构造器就实例化了类。对单例的类无法阻止生成多个实例。破坏了单例。
解决方案就是使用保护性反序列化方法readResolve

protected Object readResolve(){
            return getInstance();
        }

序列化中的异常

异常

原因

InvalidClassException

以下原因导致的无法还原实例时,1.serialVersionUID 不一致 2.同一属性的基础类型改变时 3.最近的未序列化的父类没有无参构造器 4.Externalizable 的类没有无参构造器

StreamCorruptedException、InvalidObjectException

一般是数据损坏,或协议不匹配

OptionalDataException

属性本来是基础类型,被改成了引用类型

建议及最佳实践

  • 一定要加serialVersionUID
  • readObject,writeObject最好同时存在,读写数据时保持顺序一致
  • readObject,writeObject必须调用默认的序列化方法
  • 可序列化的对象添加无参构造器
  • 了解transient关键字的常用功能