java序列化

  在做了几年java之后,越来越感觉出java基础的重要性。很多时候,如果基础不好,我们就很难真正看懂一些代码,不知道为什么人家那么写,为什么那么设计;在系统出现问题的时候,也不知道如何去做系统调优,因为我们根本就不了解代码是如何解析的,jvm是如何运作的,一些设计跟优化的事情也就无从谈起。

  最近在读红楼梦,曹老先生回味自己的人生,觉得“实愧则有余,悔又无益”于是写了一本书;曹老先生我等自然比不了,但回味自己这几年,感觉就是瞎混过来的,一碰到啥深层次的知识,基本就是“不会”二字,吾虽不才,亦觉心中有愧,于是准备翻书、网查,准备补一下自己欠下的技术债。

---------------正文分割线--------------------------------------------------------------------------------------------------------------------------------------

  我们都知道,java的序列化就是把java对象转换成为字节码,反序列化就是把java对象由字节码文件转换为jvm中的java对象。貌似我们已经了解了其中的精髓,,,那么,问题来了:我们在把数据持久化到数据库的时候,实体类会实现Serializable接口,并且经常会有这么一行:

private static final long serialVersionUID = 7439843002944792140L;

  为什么会这样,我们不实现Serializable接口行不行,我们不要这个序列化id行不行?是不是不实现这个接口就无法保存数据到数据库了?

  实诚人不卖关子,直接说答案,不是。经过测试(hibernate),即使没有实现这个接口,一样可以保存数据到数据库中,也也可正常查询数据。因为数据库的持久化实际是保存的单个属性到具体的数据库表字段,而不是存整个对象的二进制文件。

      那么这个序列化到底干什么用的呢?

  聪明的我想到了度娘,看了几篇文章后,得到如下信息:序列化就是我们开始理解的那样,把java对象转为字节码进行存储或者传输,反序列化我们理解的也对。而 serialVersionUID的作用就是兼容性校验。

  主要要了解的地方:serialVersionUID是根据类名、接口名、成员方法、以及属性等来生成的一个64位的哈希字段。也就是说如果我们改动了类名(接口名)、方法、属性等,那么这个字段的值也会变化,按这个道理来说都话,基本就是我们改了类的任何地方都会引起变化(注释改动是否会导致变化?稍后测试)。如果没有这个值,你在序列化一个对象之后,改动了该类的字段或者方法名之类的,那如果你再反序列化想取出之前的那个对象时,就会发现该类的serialVersionUID的值和之前保存的文件中的serialVersionUID的值不一样,就会抛出异常。而显式的设置这个serialVersionUID的值,就可以保证版本的兼容性。如果你在类中写上了这个值,就算类变动了,它发序列化的时候也能和文件中的原值匹配上。而新增的值则会设置成null,删除的值则不会显示。

      到此为止,序列化的基本内容已经结束了,接下来是测试,我们新建一个实体类: 

public class SerializationTestClass implements Serializable{
    private String localId;
    private String localName;
    public SerializationTestClass(){}

    public String getLocalId() {
        return localId;
    }
    public void setLocalId(String localId) {
        this.localId = localId;
    }
    public String getLocalName() {
        return localName;
    }
    public void setLocalName(String localName) {
        this.localName = localName;
    }
}

  然后测试类中单元测试:

/**
 * 把java对象序列化到文件
 */
@Test
public void testSerialization(){
    SerializationTestClass st1 = new SerializationTestClass();
    st1.setLocalId( "myid 1" );
    st1.setLocalName( "myname 1" );

    //序列化st1到xuliehua.txt文件中
    try {
        FileOutputStream fos = new FileOutputStream(new File("/Users/nevermore/Desktop/xuliehua.txt"));
        ObjectOutputStream os = new ObjectOutputStream(fos);

        os.writeObject(st1);
        os.flush();
        os.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

/**
 *反序列化文件为java对象
 */
@Test
public void testDesSerialization(){
    //序列化st1到xuliehua.txt文件中
    try {
        FileInputStream fis = new FileInputStream(new File("/Users/nevermore/Desktop/xuliehua.txt"));
        ObjectInputStream ois = new ObjectInputStream(fis);

        SerializationTestClass st2 = (SerializationTestClass)ois.readObject();
        System.out.println(st2.getLocalId()+", "+st2.getLocalName());
        ois.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

  先后运行两个单元测试方法,可以看到反序列化打印出的值,跟我们序列化之前set进去的是一样的;

      注意的是此时的实体类中并没有serialVersionUID,它使用的是jdk默认生成的serialVersionUID。我们改一下这个持久化实体类,添加一个属性addNew,仍然用原来的序列化文件进行反序列化,会报异常:java.io.InvalidClassException: com.nevermore.serialization.SerializationTestClass; local class incompatible: stream classdesc serialVersionUID = -8732474590023544793, local class serialVersionUID = -475121565243726286 。说的是本地的serialVersionUID = -475121565243726286跟序列化文件的serialVersionUID = -8732474590023544793不一样,所以异常。

  这就是所谓的版本校验问题。如果我们想兼容同一类的序列化文件,只要手动设置这个serialVersionUID不变即可(不作测试)。注释的改动会引起serialVersionUID变动吗?本地测试,先序列化了一个SerializationTestClass,然后改动了addNew变量的注释,再运行反序列化测试方法,结果正常反序列化。说明修改注释并不能改变erialVersionUID的值。

  另外,瞬态属性跟静态属性也要注意:给类添加一个瞬态属性private transient String password,序列化:

/**
 * 把java对象序列化到文件
 */
@Test
public void testSerialization(){
    SerializationTestClass st1 = new SerializationTestClass();
    st1.setLocalId( "myid 1" );
    st1.setLocalName( "myname 1" );
    st1.setPassword( "password 1" );

    //序列化st1到xuliehua.txt文件中
    try {
        FileOutputStream fos = new FileOutputStream(new File("/Users/nevermore/Desktop/xuliehua.txt"));
        ObjectOutputStream os = new ObjectOutputStream(fos);

        os.writeObject(st1);
        os.flush();
        os.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

/**
 *反序列化文件为java对象
 */
@Test
public void testDesSerialization(){
    //序列化st1到xuliehua.txt文件中
    try {
        FileInputStream fis = new FileInputStream(new File("/Users/nevermore/Desktop/xuliehua.txt"));
        ObjectInputStream ois = new ObjectInputStream(fis);

        SerializationTestClass st2 = (SerializationTestClass)ois.readObject();
        System.out.println(st2.getLocalId()+", "+st2.getLocalName()+", "+st2.getPassword());
        ois.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

  运行结果为:

  

  可以看到,瞬态属性并没有被序列化。静态的属性实际也是不会序列化的,因为这个属性是从属于类的而不是具体的对象,即使序列化了也没用意义。测试设置一个静态属性country,序列化前值为“USA”,然后序列化,然后改动值为“CN”,再进行反序列化,会发现这个值是CN。

  ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  回忆总结一下:就是java搞了一个可以把对象存到硬盘的东西叫序列化,当然我们还得能把它变成jvm里边的东西,这个叫反序列化。怎么确保不会把A序列化后的东西反序列化成了B呢,java搞了一个serialVersionUID,如果不一样我就报异常(默认同一个类,你改了都不行),怎么解决兼容问题呢,手动设置serialVersionUID。那么问题又来了,如果serialVersionUID相同,类名不一样,会如何?显然,不行,会报类型转换异常。其实序列化的数据是有结构的,类似于类名,serialVersionUID之类的信息都会保存在头消息中,用于校验,一旦校验不通过,就会报异常。

  还有个接口叫Externalizable,这个通过重写writeExternal 和 readExternal两个方法可以控制序列化的一些细节;没有进行深入学习。

posted @ 2016-12-19 14:25  facelessvoidwang  阅读(401)  评论(0编辑  收藏  举报