JAVA 对象序列化——Serializable

  • Java的对象序列化是指将那些实现了Serializable接口的对象转换成一个字符序列,并能够在以后将这个字节序列完全恢复为原来的对象。这一过程甚至可通过网络进行,这意味着序列化机制能自动弥补不同操作系统之间的差异。 只要对象实现了Serializable接口(记住,这个接口只是一个标记接口,不包含任何的方法

  • 如果我们想要序列化一个对象,首先要创建某些OutputStream(如FileOutputStream、ByteArrayOutputStream等),然后将这些OutputStream封装在一个ObjectOutputStream中。这时候,只需要调用writeObject()方法就可以将对象序列化,并将其发送给OutputStream(记住:对象的序列化是基于字节的,不能使用Reader和Writer等基于字符的层次结构)。而反序列的过程(即将一个序列还原成为一个对象),需要将一个InputStream(如FileInputstream、ByteArrayInputStream等)封装在ObjectInputStream内,然后调用readObject()即可。

  • 对象序列化过程不仅仅保存单个对象,还能追踪对象内所包含的所有引用,并保存那些对象(这些对象也需实现了Serializable接口)。下面这段代码演示了此过程:

package test.serializable;

/**
 *只是一个简单的类,用于测试序列化
 */
import java.io.Serializable;

public class Data implements Serializable {
    private static final long serialVersionUID = 7247714666080613254L;
    public int n;
    public Data(int n) {
        this.n = n;
    }
    public String toString(){
        return Integer.toString(n);
    }
}
package test.serializable;

import java.io.Serializable;
import java.util.Random;

/**
 * 
 * @author chenfei
 *
 * 用于测试序列化,每个对象Worm对象都与worm中的下一段链接,
 * 同时又有属于不同类(Data)的对象引用数组链接
 */
public class Worm implements Serializable {
    private static final long serialVersionUID = 5468335797443850679L;
    private Data[] d = {
            new Data(random.nextInt(10)),
            new Data(random.nextInt(10)),
            new Data(random.nextInt(10))
    };
    private static Random random = new Random(47);
    private Worm next;
    private char c;
    
    public Worm(int i , char x) {
        System.out.println("Worm constructor:" +i);
        c = x;
        if(--i > 0) {
            next = new Worm(i , (char)(x+1));
        }
    }
    public Worm() {
        System.out.println("Default constructor!");
    }
    
    public String toString() {
        StringBuilder sb = new StringBuilder(":");
        sb.append(c);
        sb.append("(");
        for(Data data : d) {
            sb.append(data);
        }
        sb.append(")");
        if(next!=null) {
            sb.append(next);
        }
        return sb.toString();
    }
}
package test.serializable;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;


public class SerializableTest {
    
    public static void main(String[] args) throws FileNotFoundException,
        IOException, ClassNotFoundException {
        Worm w = new Worm(6 ,'a');
        System.out.println("序列化操纵之前");
        System.out.println("w="+w);
        
        //序列化操作1--FileOutputStream
        ObjectOutputStream oos1 = new ObjectOutputStream(new FileOutputStream("worm.out"));
        oos1.writeObject("Worm storage By FileOutputStream ");
        oos1.writeObject(w);//必须所有引用的对象都实现序列化(本例终究是Data这个类)
        //否则抛出有java.io.NotSerializableException:这个异常
        oos1.close();
        
        //反序列化操作1---FileInputStream
        ObjectInputStream ois1 = new ObjectInputStream(new FileInputStream("worm.out"));
        String s1 = (String)ois1.readObject();
        Worm w1 = (Worm)ois1.readObject();
        ois1.close();
        System.out.println("反序列化操作1之后");
        System.out.println(s1);
        System.out.println("w1:"+w1);
        
        //序列化操作2--ByteArrayOutputStream
        ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream();
        ObjectOutputStream oos2 = new ObjectOutputStream(byteOutStream);
        oos2.writeObject("Worm storage By ByteOutputStream ");
        oos2.writeObject(w);
        oos2.flush();
        
        //反序列操作2--ByteArrayInputStream
        ByteArrayInputStream byteInStream = new ByteArrayInputStream(byteOutStream.toByteArray());
        ObjectInputStream ois2 = new ObjectInputStream(byteInStream);
        String s2 = (String)ois2.readObject();
        Worm w2 = (Worm)ois2.readObject();
        ois2.close();
        System.out.println("反序列化操作2之后");
        System.out.println(s2);
        System.out.println("w2:"+w2);
    }
}

运行结果如下:

Worm constructor:6
Worm constructor:5
Worm constructor:4
Worm constructor:3
Worm constructor:2
Worm constructor:1
序列化操纵之前
w=:a(853):b(119):c(802):d(788):e(199):f(881)
反序列化操作1之后
Worm storage By FileOutputStream 
w1::a(853):b(119):c(802):d(788):e(199):f(881)
反序列化操作2之后
Worm storage By ByteOutputStream 
w2::a(853):b(119):c(802):d(788):e(199):f(881)

思考:

1)反序列化后的对象,需要调用构造函数重新构造吗?
 答案:不需要。对于Serializable对象,对象完全以它存储的二进制位作为基础来构造,而不调用构造器

请看下面这段代码

package test.serializable;

import java.io.Serializable;
import java.util.Date;

/**
 * 
 * @author chenfei
 * 
 * 用于测试序列化时的deep copy
 *
 */
public class House implements Serializable {
    private static final long serialVersionUID = -6091530420906090649L;
    
    private Date date = new Date(); //记录当前的时间
    
    public String toString() {
        return "House:" + super.toString() + ".Create Time is:" + date;
    }

}
package test.serializable;

import java.io.Serializable;

public class Animal implements Serializable {
    private static final long serialVersionUID = -213221189192962074L;
    
    private String name;
    
    private House house;
    
    public Animal(String name , House house) {
        this.name = name;
        this.house = house;
        System.out.println("调用了构造器");
    }
    
    public String toString() {
        return  name + "[" +super.toString() + "']" + house;
    }

}
package test.serializable;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;


public class Myworld {

    /**
     * @param args
     * @throws IOException 
     * @throws ClassNotFoundException 
     */
public static void main(String[] args) throws IOException, ClassNotFoundException {
    House house = new House();
    System.out.println("序列化前");
    Animal animal = new Animal("test",house);
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(out);
    oos.writeObject(animal);
    oos.flush();
    oos.close();

    System.out.println("反序列化后");
    ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(in);
    Animal animal1 = (Animal)ois.readObject();
    ois.close();    
    }

}

运行结果如下所示:

序列化前
调用了构造器
反序列化后
从上面的结果中可以看到,在序列化前,当我们使用

Animal animal = new Animal("test",house);
时,调用了Animal的构造器(打印了输出语句),但是反序列后并没有再打印任何语句,说明并没有调用构造器。

2)序列前的对象与序列化后的对象是什么关系?是("=="还是equal?是浅复制还是深复制?)
 答案:深复制,反序列化还原后的对象地址与原来的的地址不同。 我们还是看上面思考1)中给出的代码,
           前两个类不变化,修改第三个类(MyWorld.java)的部分代码

修改后的代码如下:

package test.serializable;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;


public class Myworld {

    /**
     * @param args
     * @throws IOException 
     * @throws ClassNotFoundException 
     */
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        House house = new House();
        System.out.println("序列化前");
        Animal animal = new Animal("test",house);
        System.out.println(animal);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(out);
        oos.writeObject(animal);
        oos.writeObject(animal);//在写一次,看对象是否是一样,
        oos.flush();
        oos.close();
        
        ByteArrayOutputStream out2 = new ByteArrayOutputStream();//换一个输出流
        ObjectOutputStream oos2 = new ObjectOutputStream(out2);
        oos2.writeObject(animal);
        oos2.flush();
        oos2.close();

        System.out.println("反序列化后");
        ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(in);
        Animal animal1 = (Animal)ois.readObject();
        Animal animal2 = (Animal)ois.readObject();
        ois.close();
        
        ByteArrayInputStream in2 = new ByteArrayInputStream(out2.toByteArray());
        ObjectInputStream ois2 = new ObjectInputStream(in2);
        Animal animal3 = (Animal)ois2.readObject();
        ois2.close();
        
        System.out.println("out流:" +animal1);
        System.out.println("out流:" +animal2);
        System.out.println("out2流:" +animal3);
        
        
        System.out.println("测试序列化前后的对象 == :"+ (animal==animal1));
        System.out.println("测试序列化后同一流的对象:"+ (animal1 == animal2));
        System.out.println("测试序列化后不同流的对象==:" + (animal1==animal3));
        
    }

}

运行结果如下:

序列化前
调用了构造器
test[test.serializable.Animal@bb7465']House:test.serializable.House@d6c16c.Create Time is:Sat Apr 06 00:11:30 CST 2013
反序列化后
out流:test[test.serializable.Animal@4f80d6']House:test.serializable.House@193722c.Create Time is:Sat Apr 06 00:11:30 CST 2013
out流:test[test.serializable.Animal@4f80d6']House:test.serializable.House@193722c.Create Time is:Sat Apr 06 00:11:30 CST 2013
(与上面的相同)
out2流:test[test.serializable.Animal@12cc95d']House:test.serializable.House@157fb52.Create Time is:Sat Apr 06 00:11:30 CST 2013
(与上面只是值相同,但是地址不一样。)
测试序列化前后的对象 == :false
测试序列化后同一流的对象:true
测试序列化后不同流的对象==:false

从结果可以看到

序列化前后对象的地址不同了,但是内容是一样的,而且对象中包含的引用也相同。换句话说,通过序列化操作, 我们可以实现对任何可Serializable对象的”深度复制(deep copy)

——这意味着我们复制的是整个对象网,而不仅仅是基本对象及其引用。对于同一流的对象,他们地址是相同,说明他们是同一个对象,但是与其他流的对象地址却不相同
也就说,只要将对象序列化到单一流中,就可以恢复出与我们写出时一样的对象网,而且只要在同一流中,对象都是同一个。

补充:

serialVersionUID 的作用?
  • 在Java中,软件的兼容性是一个大问题,尤其在使用到对象串行性的时候,那么在某一个对象已经被串行化了,可是这个对象又被修改后重新部署了,那么在这种情况下, 用老软件来读取新文件格式虽然不是什么难事,但是有可能丢失一些信息。 serialVersionUID来解决这些问题,新增的serialVersionUID必须定义成下面这种形式:static final long serialVersionUID=-2805284943658356093L;。其中数字后面加上的L表示这是一个long值。 通过这种方式来解决不同的版本之间的串行话问题。

Java串行化机制定义的文件格式似乎很脆弱,只要稍微改动一下类的定义,原来保存的对象就可能无法读取。例如,下面是一个简单的类定义:

public class Save implements Serializable
{
String name;

public void save() throws IOException
{
FileOutputStream f = new FileOutputStream("foo");
ObjectOutputStream oos = new ObjectOutputStream(f);
oos.writeObject(this);
oos.close();
}
}

如果在这个类定义中增加一个域,例如final int val = 7;,再来读取原来保存的对象,就会出现下面的异常

  java.io.InvalidClassException:
Save; local class incompatible:
stream classdesc serialVersionUID = -2805284943658356093,
local class serialVersionUID = 3419534311899376629

上例异常信息中的数字串表示类定义里各种属性的编码值:

  • 类的名字(Save)。

  • 域的名字(name)。

  • 方法的名字(Save)。

  • 已实现的接口(Serializable)。

改动上述任意一项内容(无论是增加或删除),都会引起编码值变化,从而引起类似的异常警报。这个数字序列称为“串行化版本统一标识符”(serial version universal identifier),简称UID。解决这个问题的办法是在类里面新增一个域serialVersionUID,强制类仍旧使用原来的UID。新增的域必须是:

  • static:该域定义的属性作用于整个类,而非特定的对象。

  • final:保证代码运行期间该域不会被修改。

  • long:它是一个64位的数值。

也就是说,新增的serialVersionUID必须定义成下面这种形式:static final long serialVersionUID=-2805284943658356093L;。其中数字后面加上的L表示这是一个long值

当然,改动之后的类不一定能够和原来的对象兼容。例如,如果把一个域的定义从String改成了int,执行逆-串行化操作时系统就不知道如何处理该值,显示出错误信息:java.io.InvalidClassException: Save; incompatible types for field name。

PS:
serialVersionUID 的作用
介绍详解:http://www.360doc.com/content/09/1019/17/397210_7519829.shtml
原文地址:http://www.cnblogs.com/chenfei0801/archive/2013/04/05/3001149.html

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

推荐阅读更多精彩内容