Java 序列化 详解

序列化,简单来讲,就是以“流”的方式来保存对象,至于保存的目标地址,可以是文件,可以是数据库,也可以是网络,即通过网络将对象从一个节点传递到另一个节点。在 Java 的 I/O结构中,有 ObjectOutputStream 和 ObjectInputStream,它们可以实现将对象输出为二进制流,并从二进制流中获取对象,那为什么还需要序列化呢?这需要从 Java变量 的存储结构谈起,对Java来说,基础类型存储在栈上,复杂类型 (引用类型) 存储在堆中,对于基础类型来说,上述的操作时可行的,但对复杂类型来说,上述操作过程中,可能会产生重复的对象,造成错误,而序列化的工作流程如下 :
1> 通过输出流保存的对象都有一个唯一的序列号
2> 当一个对象需要保存时,先对其序列号进行检查
3> 当保存的对象中已包含该序列号时,不需要再次保存,否则,进入正常保存的流程
    正是通过序列号的机制,序列化才可以完整准确的保存对象的各个状态
    序列化保存的是对象中的各个属性的值,而不是方法或者方法签名之类的信息,对于方法或者方法签名,只要JVM能够找到正确的ClassLoader,那么就可以 invoke方法
    序列化不会保存类的静态变量,因为静态变量是作用于类型,而序列化作用于对象
简单的序列化示例
  序列化的完整过程包括两部分 :
  1> 使用ObjectOutputStream将对象保存为二进制流,这一步叫做“序列化”
  2> 使用ObjectInputStream将二进制流转换成对象,这一步叫做“反序列化”

下面我们来演示一个简单的示例,首先定义一个Person对象,它包含name和age两个信息 :
public class Person implements Serializable {
    private static final long serialVersionUID = 1636273937722329398L ;
    private String name ;
    private int age ;
    // ... setter getter

    public String toString() {
        return "Name:" + name + "; Age:" + age ;
    }
}

然后是两个公共方法,用来完成读、写对象的操作:
/**
* 序列化对象
*
* @param obj
* @param filePath
*/
private static void writeObject(Object obj, String filePath) {
    try {
        FileOutputStream fos = new FileOutputStream(filePath);
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(obj);
        os.flush();
        fos.flush();
        os.close();
        fos.close();
        System. out .println( "序列化成功!" );
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}
/**
* 反序列化对象
*
* @param filePath
* @return
*/
private static Object readObject(String filePath) {
    try {
        FileInputStream fis = new FileInputStream(filePath);
        ObjectInputStream is = new ObjectInputStream(fis);
        Object temp = is.readObject();
        fis.close();
        is.close();
        if (temp != null ) {
            System. out .println( "反序列化成功!" );
            return temp;
        }
    } catch (Exception ex) {
        ex.printStackTrace();
    }
    return null ;
}

将对象保存的二进制流输出到磁盘文件中,接下来,首先来看“序列化”的方法 :
private static void serializeTest1() {
    Person person = new Person();
    person.setName( "Zhang San" );
    person.setAge( 30 );
    System. out .println(person);
    writeObject (person, "/Users/mew/Desktop/person.obj" );
}

定义一个Person实例,然后将其保存到 /Users/mew/Desktop/person.obj 中,最后,是“反序列化”的方法 :
private static void deserializeTest1() {
    Person temp = (Person) readObject ( "/Users/mew/Desktop/person.obj" );
    if (temp != null ) {
        System. out .println(temp);
    }
}


隐藏非序列化信息
有时,业务对象中会包含很多属性,而有些属性是比较隐私的,例如年龄、银行卡号等,这些信息是不太适合进行序列化的,特别是在需要通过网络来传输对象信息时,这些敏感信息很容易被窃取,Java 使用 transient关键字 来处理这种情况,针对那些敏感的属性,只需使用 transient关键字 进行修饰,那么在序列化时,对应的属性值就不会被保存

定义一个新的Person2,其中 age信息是不希望序列化的 :
public class Person2 implements Serializable {
    private static final long serialVersionUID = 5871783399896345071L ;
    private String name ;
    private transient int age ;
    // ... setter getter

    public String toString() {
        return "Name:" + name + "; Age:" + age ;
    }
}
注 : 在需要隐藏非序列化的属性添加 transient

下面是“序列化”和“反序列化”的方法 :
private static void serializeTest2() {
    Person2 person = new Person2();
    person.setName( "Zhang San" );
    person.setAge( 30 );
    System. out .println(person);
    writeObject (person, "/Users/mew/Desktop/person2.obj" );
}

private static void deserializeTest2() {
    Person2 temp = (Person2) readObject ( "/Users/mew/Desktop/person2.obj" );
    if (temp != null ) {
        System. out .println(temp);
    }
}

显示结果 :
Name:Zhang San; Age:30
序列化成功!
反序列化成功!
Name:Zhang San; Age:0

可以看到经过反序列化的对象,age的信息变成 int 的默认值 0


自定义序列化过程
可以对序列化的过程进行定制,进行更细粒度的控制,思路是在业务模型中添加 readObject 和 writeObject方法

新建一个类型,叫 Person3 :
public class Person3 implements Serializable {
    private String name ;
    private transient int age ;
    // ... setter getter

    public String toString() {
        return "Name:" + name + "; Age:" + age ;
    }
    private void writeObject(ObjectOutputStream os) {
        try {
            os.defaultWriteObject();
            os.writeObject( this . age );
            System. out .println( this );
            System. out .println( "序列化成功!" );
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
    private void readObject(ObjectInputStream is) {
        try {
            is.defaultReadObject();
            this .setAge((Integer) is.readObject() - 1 );
            System. out .println( "反序列化成功!" );
            System. out .println( this );
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}
注 : readObject 和 writeObject方法,它们都是 private的,接受的参数是 ObjectStream,然后在方法体内调用了defaultReadObject或者defaultWriteObject方法

这里age同样是transient的,但是在保存对象的过程中,单独对其进行保存,在读取时,将age信息读取出来,并进行减1处理
private static void serializeTest3() {
    private static final long serialVersionUID = - 6943380636216410024L ;
    Person3 person = new Person3();
    person.setName( "Zhang San" );
    person.setAge( 30 );
    try {
        FileOutputStream fos = new FileOutputStream( "/Users/mew/Desktop/person3.obj" );
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(person);
        fos.close();
        os.close();
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

private static void deserializeTest3() {
    try {
        FileInputStream fis = new FileInputStream( "/Users/mew/Desktop/person3.obj" );
        ObjectInputStream is = new ObjectInputStream(fis);
        is.readObject();
        fis.close();
        is.close();
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

输出结果如下 :
Name:Zhang San; Age:30
序列化成功!
反序列化成功!
Name:Zhang San; Age:29
注 : 通过结果可以观察出,使用自定义序列化过程会忽略  transient 关键字的影响,这点和  Externalizable 接口 有些类似


Externalizable 接口
实现序列化的第二种方式为实现接口Externalizable,该接口继承于 java.io.Serializable 并增加了两个方法 :
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

writeExternal()方法 里定义哪些属性可以序列化,哪些不可以序列化,所以,对象在经过这里就把规定能被序列化的序列化保存文件,不能序列化的不处理,然后在反序列的时候自动调
readExternal()方法 根据序列顺序挨个读取进行反序列,并自动封装成对象返回,然后在测试类接收,就完成反序列

新建一个类型,叫 Person4 :
public class Person4 implements Externalizable {
    private static final long serialVersionUID = 2386135814450433036L ;
    private String name ;
    private transient int age ;
    // ... setter getter

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject( this . name );
        out.writeObject( this . age );
        out.writeObject( new Date());
        System. out .println( this );
        System. out .println( "序列化成功!" );
    }
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        // 注意序列化的先后顺序
        this . name = (String) in.readObject();
        this . age = (Integer) in.readObject() - 2 ;
        SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd" );
        Date date = (Date) in.readObject();
        System. out .println( "反序列化后的日期为:" + sdf.format(date));
        System. out .println( "反序列化成功!" );
        System. out .println( this );
    }
    @Override
    public String toString() {
        return "Person4{name='" + name + " \' , age=" + age + '}' ;
    }
}

这里age同样是transient的,但是在保存对象的过程中,单独对其进行保存,在读取时,将age信息读取出来,并进行减2处理,并添加时间参数
private static void serializeTest4() {
    Person4 person = new Person4();
    person.setName( "Zhang San" );
    person.setAge( 30 );
    try {
        FileOutputStream fos = new FileOutputStream( "/Users/mew/Desktop/person4.obj" );
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(person);
        fos.close();
        os.close();
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

private static void deserializeTest4() {
    try {
        FileInputStream fis = new FileInputStream( "/Users/mew/Desktop/person4.obj" );
        ObjectInputStream is = new ObjectInputStream(fis);
        is.readObject();
        fis.close();
        is.close();
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}


transient 关键字
1> 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问
2> transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口
3> 被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化 (有时候也有这个静态值,但是其不适来源于反序列化)

若实现的是Serializable接口,则所有的序列化将会自动进行此时进行序列化会忽略掉所有 transient 修饰的关键字,若实现的是Externalizable接口 或 使用 java.io.Serializable自定义序列化过程,则没有任何东西可以自动序列化,需要在writeExternal方法中进行手工指定所要序列化的变量,这与是否被transient修饰无关


serialVersionUID
对象实现 Serializable接口 时,该接口可以生成 serialVersionUID,有两种方式来生成 serialVersionUID,一种是固定值 : 1L,一种是经过 JVM计算,不同的JVM采取的计算算法可能不同

下面就是两个serialVersionUID的示例 :
private static final long serialVersionUID = 1L ;
private static final long serialVersionUID = 1636273937722329398L ;
第一行是采用固定值生成的;第二行是JVM经过计算得出的

可以使用它来控制版本兼容,如果采用JVM生成的方式,可以看到,当业务对象的代码保持不变时,多次生成的 serialVersionUID 也是不变的,当对属性进行修改时,重新生成的 serialVersionUID 会发生变化,当对方法进行修改时,serialVersionUID不变。这也从另一个侧面说明,序列化是作用于对象属性上的

1> 当没有 serialVersionUID 时,进行序列化的对象在进行反序列化是可以的,但是当属性发生改变时 将会抛出 java.io.InvalidClassException
2> 当有 serialVersionUID 时,进行序列化的对象在进行反序列化是可以的,但是当属性发生改变时 也能正常运行,但是此时重新生成一次 serialVersionUID 再次反序列化之前 序列化对象将会抛出 java.io.InvalidClassException
可以通过观察抛出的异常可以看到进行反序列化时,设置 serialVersionUID 一定要与对应已序列化对象的 serialVersionUID 一致


有继承结构的序列化
在有继承层次的业务对象,进行序列化时,如果子类实现实现Serializable接口而父类没有实现Serializable接口,那么父类必须显示提供默认构造函数否则将会抛出 java.io.InvalidClassException


实现序列化的其它方式
1> 是把对象包装成JSON字符串传输 : 可以使用 gson、FastJSON
2> 采用谷歌的ProtoBuf

JSON : 跨语言、格式清晰一目了然,但是字节数比较大,需要第三方类库 
Object Serialize java : 原生方法不依赖外部类库 字节数比较大,不能跨语言 
Google protobuf : 跨语言、字节数比较少,但是需要编写.proto配置用protoc工具生成对应的代码 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值