序列化和反序列化

序列化简介

Java序列化,一个日常开发中比较少用到的技术。正常情况下,JVM启动后,我们可以创建对象,JVM关闭后,我们创建过的对象都随之销毁,资源释放。但有些时候可能要求在JVM停止之后,某些对象需要保存起来,以便将来再重新读取它们。

举个例子,应用服务器的HttpSession对象,Session是指浏览器与服务器之间的一次会话,对应的是服务器中的一个Session对象,而客户端中保存一个jsessionid。那么当某种情况下,我们不得不重启服务器的时候,就需要把之前所有的Session对象保存起来,服务器重启之后,将这些Session对象再重新加载过来,这样避免了之前浏览器与服务器建立的会话失效,在浏览器那看来,就好象服务器没有关闭过一样(假设服务器重启期间用户没有操作)。这就用到了Java序列化技术,关于这个例子,我们可以拿Tomcat来测试一下,注意要用正常的手段来关闭服务器(shutdown.bat),而非强制关闭,强制关闭没有序列化的过程。

Java序列化

首先创建一个可序列化的JavaBean类:Name.java

import java.io.Serializable;  
  
/** 
 * 可序列化的类,需要实现Serializable接口 
 */  
public class Name implements Serializable {  
  
    private String firstname;  
      
    private String lastname;  
      
    public Name() {  
        System.out.println("无参构造器");  
    }  
  
    public Name(String firstname, String lastname) {  
        System.out.println("全参构造器");  
        this.firstname = firstname;  
        this.lastname = lastname;  
    }  
  
    //省略getter和setter

    @Override  
    public String toString() {  
        return "我的名字是" + firstname + "," + lastname;  
    }  
      
}  

再实现一个序列化的工具类:Serializations.java

import java.io.FileInputStream;  
import java.io.FileOutputStream;  
import java.io.IOException;  
import java.io.ObjectInputStream;  
import java.io.ObjectOutputStream;  
  
/** 
 * 序列化工具类 
 */  
public class Serializations {  
  
    /** 
     * 序列化对象到指定路径文件 
     * @param outPath 文件路径 
     * @param outObj 需要序列化的对象 
     * @throws IOException 当I/O发生异常时 
     */  
    public static void serialize(String outPath, Object outObj) 
        throws IOException {  
        ObjectOutputStream oos = null;  
        try {  
            oos = new ObjectOutputStream(new FileOutputStream(outPath));  
            oos.writeObject(outObj);  
        } finally {  
            if(oos != null)    oos.close();  
        }  
    }  
      
    /** 
     * 从文件中逆序列化出对象 
     * @param inPath 文件路径 
     * @return 你序列化出的对象 
     * @throws IOException 当I/O发生异常时 
     * @throws ClassNotFoundException 当文件中不存在序列化的对象时 
     */  
    public static Object deserialize(String inPath) 
        throws IOException, ClassNotFoundException {  
        ObjectInputStream ois = null;  
        try {  
            ois = new ObjectInputStream(new FileInputStream(inPath));  
            return ois.readObject();  
        } finally {  
            if(ois != null)    ois.close();  
        }  
    }  
}  

最后创建两个个测试类,来使用一下序列化方法和逆序列化方法:WriteObject.java。

import java.io.IOException;  
  
public class WriteObject {  
  
    public static void main(String[] args) throws IOException {  
        Name name = new Name("科比", "布莱恩特");  
        Serializations.serialize(args[0], name);  
    }  
}  

运行后,指定目录下会生成相应文件,其内包含了name对象信息。

ReadObject.java:

import java.io.IOException;  
  
public class ReadObject {  
  
    public static void main(String[] args) 
        throws ClassNotFoundException, IOException {  
        Object obj = Serializations.deserialize(args[0]);  
        System.out.println(obj);  
    }  

运行后,输出:我的名字是科比,布莱恩特

我们成功的将name对象序列化到了指定文件中,并且通过逆序列化得到一个和原对象属性相同的对象。注意,逆序列化出的对象没有使用该对象的构造器(由输出结果可以证明),并且和原对象不相等。

对象的默认序列化机制
序列化时,对象的类、类的签名,以及类及其所有超类型的非瞬态(non-transient)和非静态(non-static)字段的值都将被写入。逆序列化时,对象的类、类的签名,以及类及其所有超类型的非瞬态(non-transient)和非静态(non-static)字段的值都将被读取。如果我们想某个成员变量不被序列化,可以在其前面加入transient关键字。如:

private transient String lastname;  

序列化版本号

如果对象所属类在对象序列化之后做了修改,比如修改属性名称、类型、修饰符等等,再次逆序列化就会发生异常,如我们将lastname前加入transient,使用ReadObject.java进行逆序列化, 将会抛出如下异常:

Exception in thread "main" java.io.InvalidClassException: Name; local class incompatible: stream classdesc serialVersionUID = 3999552307707967101, local class serialVersionUID = -4860856635192050881  
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:604)  
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1601)  
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1514)  
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1750)  
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1347)  
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:369)  
    at Serializations.deserialize(Serializations.java:43)  
    at ReadObject.main(SerializationsTest.java:14)  

异常的大概描述是说流中的类的版本号和本地类的版本号不一致,这里要引入一个序列化版本号(serialVersionUID)的概念,serialVersionUID是一个64位的值,在类中需要声明为private static final long,它可以人为来维护,也可以通过JVM实现的算法来生成,安装JDK后,可以过%JAVA_HOME%/bin/serialver.exe来生成serialVersionUID。在逆序列化时,会将从对象流中读取的类信息和当前classpath下的相应类的类信息(Name.class)进行比对,比对的媒介就是serialVersionUID,如果对象中没有声明serialVersionUID,那么该值就会通过默认的算法生成,两端不一致时,就会抛出上面的异常,逆序列化失败。

当编写一个可序列化的类时(Name.java),可以给serialVersionUID赋一个即简单又易理解的值,如:

private static final long serialVersionUID = 1L;  

如果对该类进行了更改,可能需要同时更新serialVersionUID,如:

private static final long serialVersionUID = 2L;  

但有时我们可能即使更改了类之后,仍然要保持之前序列化的可逆性,也就是对之前的序列化文件做个兼容,那么就不能更新serialVersionUID的值,这时更改前生成的序列化文件依然可逆序列化,那么其更新的字段会以字段类型的预设值逆序列化,避开不兼容的问题。

复合类序列化

上文中实现了序列化和逆序列化一个简单的Name对象,下面来看一个稍复杂的情况,Name类中复合了其它类。

import java.io.Serializable;  
  
/** 
 * 可序列化的类,需要实现Serializable接口 
 */  
public class Name implements Serializable {  
  
    private static final long serialVersionUID = 1L;  
  
    private String firstname;  
      
    private String lastname;  
      
    private Nickname nickname;  
      
    public Name() {}  
  
    public Name(String firstname, String lastname) {  
        this.firstname = firstname;  
        this.lastname = lastname;  
    }  
      
    public Name(String firstname, String lastname, Nickname nickname) {  
        this.firstname = firstname;  
        this.lastname = lastname;  
        this.nickname = nickname;  
    }  
  
    public String getFirstname() {  
        return firstname;  
    }  
  
    public void setFirstname(String firstname) {  
        this.firstname = firstname;  
    }  
  
    public String getLastname() {  
        return lastname;  
    }  
  
    public void setLastname(String lastname) {  
        this.lastname = lastname;  
    }  
      
    public Nickname getNickname() {  
        return nickname;  
    }  
  
    public void setNickname(Nickname nickname) {  
        this.nickname = nickname;  
    }  
  
    @Override  
    public String toString() {  
        return "我的名字是" + firstname + "," + lastname + "\n我的昵称是" + nickname;  
    }  
      
}  

Nickname.java:

import java.io.Serializable;  
  
/** 
 * 昵称类 
 */  
public class Nickname implements Serializable {  
  
    private static final long serialVersionUID = 1L;  
      
    private String name;  
      
    public Nickname() {}  
  
    //省略getter和setter
  
    @Override  
    public String toString() {  
        return name;  
    }  
      
}  

WriteObject.java:

import java.io.IOException;  
  
import com.runqianapp.test.bean.Name;  
import com.runqianapp.test.bean.Nickname;  
  
public class WriteObject {  
  
    public static void main(String[] args) throws IOException {  
        Nickname nickname = new Nickname("黑曼巴");  
        Name name = new Name("科比", "布莱恩特", nickname);  
        Serializations.serialize(args[0], name);  
    }  
}  

运行后,指定目录下会生成相应文件,再次运行ReadObject.java,会得到如下输出信息:

我的名字是科比,布莱恩特  
我的昵称是黑曼巴  

在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化,以此类推。在序列化过程中,可能会遇到不支持可序列化接口的对象,在此情况下,将抛出 NotSerializableException,并将标识不可序列化对象的类。如将Nickname.java去掉Serializable接口,再次运行WriteObject.java,会抛出如下异常:

Exception in thread "main" java.io.NotSerializableException: Nickname  
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1180)  
    at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1528)  
    at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1493)  
    at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1416)  
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1174)  
    at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:346)  
    at Serializations.serialize(Serializations.java:26)  
    at WriteObject.main(WriteObject.java:13)  

我们可以用transient来修饰nickname属性,这样该类就可以正常序列化了,但是nickname中的属性也就无法序列化了,那我们如何让不能序列化的类NickName中的name属性可以序列化和反序列化呢?在序列化和反序列化过程中需要特殊处理的类必须使用下列准确签名来实现特殊方法:

public class Name implements Serializable {  
    ...  
    transient private Nickname nickname; 
    ...   
  
    private void writeObject(ObjectOutputStream out) throws IOException {  
        // 默认序列化机制  
        out.defaultWriteObject();  
        // 序列化nickname中的name属性  
        out.writeObject(nickname.getName());  
    }  
  
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
        // 默认逆序列化机制  
        in.defaultReadObject();  
        // 逆序列化一个nickname对象  
       nickname = new Nickname(in.readObject().toString()); 
    }  
}  

这样就可以处理其不可序列化的复合类Nickname中的name属性序列化及反序列化。运行WriteObject和ReadObject,序列化和反序列化成功。这两个方法如何实现取决于最终的需求。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容