Java序列化机制

什么是序列化

  • 序列化就是指对象通过写出描述自己状态的数值来记录自己的过程,将对象的运行时数据转化为二进制流。
  • 反序列化是序列化的逆过程,将二进制流转化为对象的运行时数据。

序列化的必要性

一切都是对象,在分布式环境中经常需要将对象数据从这一端网络或设备传递到另一端。这就需要有一种可以在两端传输数据的协议。或者将对象持久化到硬盘上,等待下载加载数据并继续运行,Java序列化机制就是为了解决这样的问题而产生。

应用场景

  • Java RMI调用。即Remote Method Invocation远程方法调用,它被设计成一种面向对象的通讯方式,允许程序员使用远程对象来实现通信。既然使用远程对象进行通信,避免不了要传输Java对象,所以这些需要传输的对象必须要经过序列化。
  • Façade 模型。一种远程调用的模型。
image.png

Client 端通过 Façade Object 才可以与业务逻辑对象进行交互。而客户端的 Façade Object 不能直接由 Client 生成,而是需要 Server 端生成,然后序列化后通过网络将二进制对象数据传给 ClientClient 负责反序列化得到 Façade 对象。该模式可以使得 Client 端程序的使用需要服务器端的许可,同时 Client 端和服务器端的 Façade Object 类需要保持一致。当服务器端想要进行版本更新时,只要将服务器端的 Façade Object 类的序列化 ID 再次生成,当 Client 端反序列化 Façade Object 就会失败,也就是强制 Client 端从服务器端获取最新程序。

  • java对象的持久化。写过crud的程序员都知道,我们每天做的事情就是不停的序列化和反序列化。

如何实现序列化

怎样才能正确的实现序列化和反序列化呢? 我们应该考虑以下问题:

  1. 对象的序列化,序列化的内容是什么呢?毫无疑问,当然是对象的运行时数据咯,这肯定就不包含对象的类结构,方法体等等。除非这个对象就是Class对象。哪些有事运行时数据呢?运行时数据包含对象的非瞬态域。其实就是对象的那些不是transient关键字修饰字段啦,当然也不包含静态字段,静态字段是属于类的静态域,又不是对象的域。

  2. 序列化和反序列化的过程是什么样子的呢? 序列化就是将对象的运行时数据塞到流中,然后在反序列化时创建一个该类的空对象,然后将拿到的数据放到对应的域中即可。这里可以看到序列化的类在流的另一端是已经存在的,静态域当然不需要再传咯。静态域已经跟随类发布出去了。

  3. 哪些对象应该被序列化,哪些对象有不应该被序列化?在Java中一切皆对象,对象千千万,如果每个对象都要被传输,影响序列化的效率暂切不说,对网络和硬盘的资源也造成了浪费啊,而且有的对象传输给客户端完全没有意义,比如说说我们经常写的ServiceController层对象,里面什么数据也没有,传到另一端也没什么意义,而且影响效率浪费资源。在第一个问题中也说明了,序列化的内容是对象的数据,数据,数据。因此,含有运行时数据的对象才值得被序列化。那哪些对象是含有数据的呢。java中用Serializable接口来标记那些还有数据域的类。

  4. 如何确保反序列化得到的对象与被序列化的对象是同一类对象呢?我们希望二进制流的两端是一模一样的类,如果不是或者相似,都有可能造成严重的错误。比如说有个相似的类获取到了数据,但是这些数据是方法不需要的,就有可能导致在方法执行时出现异常。解决这一问题我们只需要在二进制流中加入序列化类的标志,在反序列化时,取得这个标志,和本地类进行比较,如果不一致直接抛出异常,避免在后续处理中出现致命错误。

  5. 一个应该被序列化对象的内部,有其他的对象引用,该怎么序列化?也就是说,若果对象的域是基本类型,那好说直接序列化就好了,如果是其他对象的引用怎么办呢,如果引用的对象属于应该被序列化的范畴,那我们继续递归的进行序列化。如果不属于应该被序列化的范畴,那么抛出异常,让程序员在这个字段上加上transient关键字。

  6. 一个应该被序列化对象的内部,会存在不该被序列化的域该怎么办?这个好理解,像上面说的这种情况,加上transient关键字即可。序列化的时候忽略他即可。

  7. 还有这样一种场景如下,a对象和b对象都含有c的引用,在反序列化时,怎么保证反序列化后的ab引用的c对象是同一个呢?这个也好办,我们在第一次序列化c的时候,给他加一个编号#1,在序列化baField域时,发现这个对象之前序列化过了,直接用#1代替就行了。反序列化同理。

AnObject a = new AnObject();
AnObject b = new AnObject();
AnObject c = new AnObject();
a.aField = c;
b.aField = c;
  1. 有继承关系的对象该如何被序列化?都知道对象除了他的那些数据域以外,还有一个其父类对象的应用,那么序列化时父类对象该怎么办呢。

序列化的实践

Java默认序列化机制非常简单,设计者为我们屏蔽了底层实现,对于程序员而言只需决定哪些对象需要被序列化。

//需要被序列化的类
public class Employee implements Serializable {
    private String name;
    public Employee(String name) {
        this.name = name;
    }
}

//序列化对象
ObjectOutput objectOutput = new ObjectOutputStream(new FileOutputStream(new File("D:Temp/object.dat")));
Employee employee = new Employee("Tom");
objectOutput.writeObject(employee);

//反序列化对象
ObjectInput objectInput = new ObjectInputStream(new FileInputStream(new File("D:Temp/object.dat")));
Object readEmployee = objectInput.readObject();

System.out.println("Object read from file is " + readEmployee);

上面是一段正正经经的序列化和反序列化代码,序列化后的文件能够成功的被反序列化。现在对上面的代码做一些改变,然后看看结果。

  1. 先将Employee对象序列化到object.dat文件中,然后改变Employee的结构,看看能不能被反序列化。改变后的Employee的结构如下。
public class Employee implements Serializable {
    private String name;
    public Employee(String name) {
        this.name = name;
    }
    // 添加一个get方法
    public void setName(String name) {
        this.name = name;
    }
}

在反序列化时抛出了异常。java.io.InvalidClassException: com.ccr.Employee; local class incompatible: stream classdesc serialVersionUID = -8417225670575338188, local class serialVersionUID = -2926582740416391491。该异常指出被序列化的文件中的serialVersionUID和本地类的serialVersionUID不同,什么是serialVersionUID呢?可以理解为类的版本序列号,在序列化时,Java会根据类的结构,包括类的域,类的方法签名等生成一个版本序列号,这个版本序列号就是类的签名,或者是指纹信息,只要类结构发生任何变化,得到的序列号都会不同,虽然Employee只加了一个get方法,但是这个类的签名信息已经发生变化,在反序列化时,检查他们的序列号不同,Java就认为类结构发生变化,不能被反序列化。

  1. 但是我只是加了一个get方法,并没影响类的数据域,不是说只是序列化数据域么,这有点不合理啊,确实,这应该是Java默认序列化机制不合理的地方,好在也有补救办法。我们可以通过一个静态的final域自定义序列号版本。如:
public class Employee implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    public Employee(String name) {
        this.name = name;
    }
    // 添加一个get方法
    public void setName(String name) {
        this.name = name;
    }
}

有点眼熟,用eclips的童鞋都知道,工具会给我们自动生成这玩意,他也是根据类签名生成的。只要保证序列化和反序列化这各类的序列号相同,就能反序列化成功,这个可以用来控制客户端的类版本,使客户端必须升级才能调用远程服务。但是问题又来了,如果两边serialVersionUID相同,纵使你把这个类改翻了天,它照样能够序列化成功,我相信这应该是Java默认序列化机制的一个缺陷。

  1. Employee的构造函数设置为private看看反序列化的效果。这里我吧Employee默认的构造函数也设置成了private,反序列化依然能够成功。这说明反序列化没有调用类的构造函数,而是使用其他的方式构造对象。
  2. 我们将Employeename域设置成transient,序列化时name = "Tom",反序列化后name = null,说明transient阻止了name的序列化。
  3. 我们验证下数据域中存在引用的情况。
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;
    //引用域
    private String name;

    Employee leader;

    public Employee(String name) {
        this.name = name;
    }

}


ObjectOutput objectOutput = new ObjectOutputStream(new FileOutputStream(new File("D:Temp/object.dat")));
Employee employee = new Employee("Tom");
//将引用域设置成自己,观察会不会相同对象重复序列化
employee.leader = employee;
objectOutput.writeObject(employee);

//反序列化时观察readEmployee对象leader域是readEmployee本省
ObjectInput objectInput = new ObjectInputStream(new FileInputStream(new File("D:Temp/object.dat")));
Object readEmployee = objectInput.readObject();
  1. 看看继承的情况,如果父类被标记为可序列化,那么子类必然可以版序列化。如果子类被标记为可序列化,父类没有被标记为序列化那又是什么情况呢。
//父类不可以被序列化
public class Employee {

    private static final long serialVersionUID = 1L;

    String name;

    public void say(){
        System.out.println("I'm father object,my name is " + name);
    }

}

//子类可以被序列化
public class Manager extends Employee implements Serializable {

    @Override
    public void say(){
        System.out.println("I'm son object,my name is " + name);
    }
}

//序列化
ObjectOutput objectOutput = new ObjectOutputStream(new FileOutputStream(new File("D:Temp/object.dat")));

Manager manager = new Manager();
manager.name = "Tom";
manager.say();

objectOutput.writeObject(manager);

//反序列化
ObjectInput objectInput = new ObjectInputStream(new FileInputStream(new File("D:Temp/object.dat")));

Manager readEmployee = (Manager) objectInput.readObject();
readEmployee.say();

//结果
I'm son object,my name is Tom
I'm son object,my name is null

可以看出这种父类不可序列化,子类可序列化的情况,Java的默认处理机制是将父类域的数据丢弃,只序列化子类域的数据。

  1. 自定义序列化。Java提供了两种方式来供我们自己定义对象的序列化方式。一种是定义private void writeObject 和private void readObject方法,另一种是实现Externalizable接口。
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;

    String name;

    transient int age;

    public void say(){
        System.out.println("my name is " + name + ". and I'm " + age + " years old.");
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        //调用默认的序列化机制,该机制会忽略age字段
        out.defaultWriteObject();
        //使用自己的方式来实现age字段的序列化
        out.writeInt(age);
    }
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        age = in.readInt();
    }

}

//序列化
ObjectOutput objectOutput = new ObjectOutputStream(new FileOutputStream(new File("D:Temp/object.dat")));

Employee employee = new Employee();
employee.name = "Tom";
employee.age = 41;
employee.say();

objectOutput.writeObject(employee);

//反序列化
ObjectInput objectInput = new ObjectInputStream(new FileInputStream(new File("D:Temp/object.dat")));

Employee readEmployee = (Employee) objectInput.readObject();

readEmployee.say();

//结果
my name is Tom. and I'm 41 years old.
my name is Tom. and I'm 41 years old.

public class Employee implements Externalizable {

    private static final long serialVersionUID = 1L;

    String name;

    int age;

    public void say(){
        System.out.println("my name is " + name + ". and I'm " + age + " years old.");
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        //我们自决定哪些字段该序列化哪些字段不该序列化,或者该怎样序列化
        //也可以调用默认的机制
        //可以在这里进行加密
        //也可以用这种机制实现超类的具体化,如果超类没有实现Serializable
        out.writeObject("aa" + name);
        out.writeInt(age + 1);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        //我们自决定哪些字段该反序列化哪些字段不该反序列化
        name = (String) in.readObject();
        age = in.readInt() - 1;
    }
}


//序列化
ObjectOutput objectOutput = new ObjectOutputStream(new FileOutputStream(new File("D:Temp/object.dat")));

Employee employee = new Employee();
employee.name = "Tom";
employee.age = 41;
employee.say();

objectOutput.writeObject(employee);

//反序列化
ObjectInput objectInput = new ObjectInputStream(new FileInputStream(new File("D:Temp/object.dat")));

Employee readEmployee = (Employee) objectInput.readObject();

readEmployee.say();

//结果
my name is Tom. and I'm 41 years old.
my name is aaTom. and I'm 41 years old.

参考这篇文章很详细

总结

写了那么多,了解序列化无非下面两点:

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

推荐阅读更多精彩内容