深入分析java序列化

概念

先来点简单的概念:

what?why?

什么是序列化?为什么要序列化?

答曰:将java对象转成字节序列,用以传输和保存

where?

使用场景是什么?

答曰:对象的传输;状态的备份,例如jvm的dump文件;

好了,不装*了,下面说的详细点。其实对象的序列化主要有两种用途:

  • 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中
  • 在网络上传送对象的字节序列

在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。

当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

实现方式

how?

  1. (需要序列化的类)实现Serializable接口。查看源码可知Serializable是个空接口(里面没有任何方法),即标记接口。作用就是明确地告诉java,这个类需要序列化,不实现这个接口,那这个对象是没法序列化和传输的(如果不加implements Serializable,会报错,建议试试,直观感受下),类似的标记接口还有cloneable。
  2. 创建输入输出流。首先,序列化一个对象,需要要创建某些OutputStream(如FileOutputStream、ByteArrayOutputStream等),然后将这些OutputStream封装在一个ObjectOutputStream中。这时候,只需要调用writeObject()方法就可以将对象序列化,并将其发送给OutputStream(对象的序列化是基于字节的,不能使用Reader和Writer等基于字符的层次结构)。其次,反序列的过程(即将一个序列还原成为一个对象),则需要将一个InputStream(如FileInputstream、ByteArrayInputStream等)封装在ObjectInputStream内,然后调用readObject()即可。简单来说即:

    序列化:ObjectOutputStream.writeObject(Object)

    反序列化:ObjectInputStream.readObject()

见下面的例子:

先定义一个待序列化的对象:

package com.alibaba.serialize.common;

import java.io.Serializable;

/*
 * 不加implements Serializable,会报错
 */
public class User implements Serializable{
    private static final long serialVersionUID = 1L;
    private String username;
    private int age;

    public User(String username, int age) {
        this.username = username;
        this.age = age;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

再写一个序列化的例子

package com.alibaba.serialize.common;

import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializeExample {

    public static final String out_file = "src/com/alibaba/serialize/temp.out";

    public static void main(String[] args) {
        User user = new User("tony", 18);
        try {
            //如果这里改成网络输出流而不是文件输出流(反序列化那里也同样改成网络输入流),则可以在网络上传输
            ObjectOutputStream out = new ObjectOutputStream(
                               new BufferedOutputStream(new FileOutputStream(out_file)));

            out.writeObject("用户信息..");
            out.writeObject(user);
            out.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("序列化完成。请调用反序列化类DeserializeExample完成反序列化!");
    }
}

最后再写一个反序列化的例子

package com.alibaba.serialize.common;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class DeserializeExample {
    public static void main(String[] args) throws Exception{
            ObjectInputStream in = new ObjectInputStream(
                               new BufferedInputStream(new FileInputStream(SerializeExample.out_file)));
            String title = (String) in.readObject();
            System.out.println(title);
            User user = (User) in.readObject();
            System.out.println("用户姓名:"+user.getUsername());
            System.out.println("用户年龄:"+user.getAge());
    }
}

分别运行SerializeExampleDeserializeExample 可以看到对应的效果。

序列化的文件关系

说几点平常不注意容易忽略的地方。

  1. 如果一个类没有实现Serializable接口,但是它的基类实现了,那么这个类也是可以序列化的;
  2. 相反,如果一个类实现了Serializable接口,但是它的父类没有实现,那么这个类还是可以序列化(Object是所有类的父类),但是序列化该子类对象,然后反序列化后输出父类定义的某变量的数值,会发现该变量数值与序列化时的数值不同(一般为null或者其他默认值),而且这个父类里面必须有无参的构造方法,不然子类反序列化的时候会报错。

见示例:

先写个父类

package com.alibaba.serialize.parent;

import java.io.Serializable;

public class CarSerialize{
    private String name;
    private Long price;

    /*
     * 没有这个无参构造函数会报错,可以删除,测试下
     */
    public CarSerialize() {
        super();
    }

    public CarSerialize(String name, Long price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getPrice() {
        return price;
    }

    public void setPrice(Long price) {
        this.price = price;
    }

}

再写个子类

package com.alibaba.serialize.parent;

import java.io.Serializable;

public class BMWSerialize extends CarSerialize implements Serializable{
    private String name;
    private Long price;

    public BMWSerialize(String name, Long price) {
        super(name, price);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getPrice() {
        return price;
    }

    public void setPrice(Long price) {
        this.price = price;
    }
}

最后写个测试类

package com.alibaba.serialize.parent;

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 SerializeTest {

    public static void main(String[] args) {
        try {
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
            out.writeObject(new BMWSerialize("BMW",100L));
            out.close();

            ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj"));
            BMWSerialize bmw = (BMWSerialize) oin.readObject();
            System.out.println("解密后的字符串:" + bmw.getName());
            oin.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

上述代码中,大家可以尝试按照我上面说的两点,分情况进行测试。

这里还有个需要注意的地方

同一个流的对象引用关系被很好地保留了下来,不同流的对象引用关系则无法保证匹配

这一点我感觉比较容易理解,就不写了,有时间可以测试下。

序列化ID

思考一个问题:如果序列化之后,两端或者版本不同,class不一致怎么办?

???

???

这里就有序列化ID的概念了,serialVersionUID适用于JAVA的序列化机制。反序列化时,如果类发生了变动,则构造器和赋值语句不会生效。

简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException(如同上面一节,你把父类的无参构造方法删除之后,再反序列化也会报这个错)。

serialVersionUID有两种显示的生成方式:

  • 一是默认的1L,比如:private static final long serialVersionUID = 1L
  • 二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:private static final long serialVersionUID = xxxxL;

再思考一个问题,如果序列化ID是一样的,假设A端是序列化,B端是反序列化,反序列化之前B端序列化对象发生变化,会有几种情况?其实逻辑还是比较容易理解的,略微总结了下,大致如下:

  • B加字段:序列化,反序列化正常,B端新增加的int字段被赋予了字段类型的默认值(如0或者false)
  • B删字段:序列化,反序列化正常(不会报错),B端字段少于A端,A端多的字段值丢失

自定义序列化

既然题目是深入分析,那当然要接着分析,哈哈~所以,再问一个问题,当对象中有敏感字段怎么办?如password。是不是自己可以决定序列化方式呢?这里解释一下

在序列化过程中,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。

基于这个原理,可以在实际应用中得到使用,用于敏感字段的加密工作,见示例(代码摘自参考资料)。

先写一个自定义序列化过程的待序列化对象。

package com.alibaba.serialize.customer;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectInputStream.GetField;
import java.io.ObjectOutputStream;
import java.io.ObjectOutputStream.PutField;
import java.io.Serializable;

public class SecurityInfo implements Serializable{
    private String password = "pass";

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    private void writeObject(ObjectOutputStream out) {
        try {
            PutField putFields = out.putFields();
            System.out.println("原密码:" + password);
            password = "encryption";//模拟加密
            putFields.put("password", password);
            System.out.println("加密后的密码" + password);
            out.writeFields();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void readObject(ObjectInputStream in) {
        try {
            GetField readFields = in.readFields();
            Object object = readFields.get("password", "");
            System.out.println("要解密的字符串:" + object.toString());
            password = "pass";//模拟解密,需要获得本地的密钥
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

再写一个测试类

package com.alibaba.serialize.customer;

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 CustomerSerialize {

    public static void main(String[] args) {
        try {
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
            out.writeObject(new SecurityInfo());
            out.close();

            ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj"));
            SecurityInfo t = (SecurityInfo) oin.readObject();
            System.out.println("解密后的字符串:" + t.getPassword());
            oin.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

ok!运行下看看吧。

其实说到自定义序列化,还可以通过实现java序列化另一个接口Externalizable,实现Externalizable接口就需要实现它的两个方法,如下:

大致总结了下,Externalizable和Serializable的区别:

实现Serializable接口 实现Externalizable
系统自动存储必要信息 程序员决定存储哪些信息
Java内建支持,易于实现,只需实现该接口如何即可,无须任何代码支持 仅仅提供两个空方法,实现该接口必须为两个空方法提供实现
性能略差 性能略高

其实虽然Externalizable性能高,但是我们一般很少在代码里看到,这个原因需要请假下别人,个人认为可能是自己写不够通用,太麻烦,而且一般敏感信息也不会这么传。

几种序列化协议比较

这里主要介绍和对比几种当下比较流行的序列化协议,包括XML、JSON、Protobuf、Thrift和Avro。这里因为我用的也不多,只用过json和xml,所以,对比的话这里列了个参考资料: http://tech.meituan.com/serialization_vs_deserialization.html

本文还参考:

http://blog.csdn.net/zhaozheng7758/article/details/7820018

感谢上述作者~

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-07 12:17:22

深入分析java序列化的相关文章

Java 序列化深入分析

序列化机制介绍 ??序列化是指把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中.序列化后的字节流保存了Java对 象的状态以及相关的描述信息.客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象.本质上讲,序列化就是把实体对象状态按照一定的格式写入到有序字节流,反序列化就是从有序字节流重建对象,恢复对象状态.序列化机制的核心作用就是对象状态的 保存与重建. Java序列化机制解析 ??Java API提供了对序列化的支持,要实

深入分析Java的序列化与反序列化

阅读目录 Java对象的序列化如何对Java对象进行序列化与反序列化序列化及反序列化相关知识ArrayList的序列化ObjectOutputStream总结 序列化是一种对象持久化的手段.普遍应用在网络传输.RMI等场景中.本文通过分析ArrayList的序列化来介绍Java序列化的相关内容.主要涉及到以下几个问题: 怎么实现Java的序列化 为什么实现了java.io.Serializable接口才能被序列化 transient的作用是什么 怎么自定义序列化策略 自定义的序列化策略是如何被调

深入分析 Java I/O 的工作机制

I/O 问题可以说是当今互联网 Web 应用中所面临的主要问题之一,因为当前在这个海量数据时代,数据在网络中随处流动.这个流动的过程中都涉及到 I/O 问题,可以说大部分 Web 应用系统的瓶颈都是 I/O 瓶颈.本文的目的正是分析 I/O 的内在工作机制,你将了解到:Java 的 I/O 类库的基本架构:磁盘 I/O 工作机制:网络 I/O 的工作机制:其中以网络 I/O 为重点介绍 Java Socket 的工作方式:你还将了解到 NIO 的工作方式,还有同步和异步以及阻塞与非阻塞的区别,最

深入分析Java Web技术(2) IO

IO是当今Web面临的主要问题之一,可以说,大部分web应用的瓶颈都是IO的瓶颈. Java的IO类是java.io.它包含有80多个类,分为4大部分: 基于字节操作: InputStream,OutputStream 基于字符操作: Writer 和Reader 基于磁盘操作: File 基于网络操作: Socket 一.基于字符的IO操作 不管是网络传输还是磁盘,最小的存储单元都是字节,而不是字符,那为什么还有字符的处理呢?这是因为我们程序中操作的单位一般都是字符,而不是字节,而将字符转换为

[转]深入分析 Java 中的中文编码问题

收益匪浅,所以转发至此 原文链接: http://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/ 深入分析 Java 中的中文编码问题 编码问题一直困扰着开发人员,尤其在 Java 中更加明显,因为 Java 是跨平台语言,不同平台之间编码之间的切换较多.本文将向你详细介绍 Java 中编码问题出现的根本原因,你将了解到:Java 中经常遇到的几种编码格式的区别:Java 中经常需要编码的场景:出现中文问题的原因分析:在开发 Java

深入分析 Java I/O 的工作机制(转载)

声明:本文转自 http://www.ibm.com/developerworks/cn/java/j-lo-javaio/ I/O 问题可以说是当今互联网 Web 应用中所面临的主要问题之一,因为当前在这个海量数据时代,数据在网络中随处流动.这个流动的过程中都涉及到 I/O 问题,可以说大部分 Web 应用系统的瓶颈都是 I/O 瓶颈.本文的目的正是分析 I/O 的内在工作机制,你将了解到:Java 的 I/O 类库的基本架构:磁盘 I/O 工作机制:网络 I/O 的工作机制:其中以网络 I/

深入分析 Java 中的中文编码问题--转

几种常见的编码格式 为什么要编码 不知道大家有没有想过一个问题,那就是为什么要编码?我们能不能不编码?要回答这个问题必须要回到计算机是如何表示我们人类能够理解的符号的,这些符号也就是我们人类使用的语言.由于人类的语言有太多,因而表示这些语言的符号太多,无法用计算机中一个基本的存储单元—— byte 来表示,因而必须要经过拆分或一些翻译工作,才能让计算机能理解.我们可以把计算机能够理解的语言假定为英语,其它语言要能够在计算机中使用必须经过一次翻译,把它翻译成英语.这个翻译的过程就是编码.所以可以想

java 序列化机制深度解析

概要 序列化机制允许将实现序列化的Java对象转换为字节序列,这些字节序列可以被保存在磁盘上或通过网络传输,以备以后重新恢复原来的对象,序列化机制使得对象可以脱离程序的运行而独立存在 可序列化的类包括:实现了Serializable的类,数组,枚举,String类也是可序列化对象 由于序列化保存的是对象的状态,因此不会保存类的静态变量 -通过ObjectOutputStream和ObjectInputStream对对象进行序列化及反序列化 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一

深入分析 Java I/O 的工作机制--转载

Java 的 I/O 类库的基本架构 I/O 问题是任何编程语言都无法回避的问题,可以说 I/O 问题是整个人机交互的核心问题,因为 I/O 是机器获取和交换信息的主要渠道.在当今这个数据大爆炸时代,I/O 问题尤其突出,很容易成为一个性能瓶颈.正因如此,所以 Java 在 I/O 上也一直在做持续的优化,如从 1.4 开始引入了 NIO,提升了 I/O 的性能.关于 NIO 我们将在后面详细介绍. Java 的 I/O 操作类在包 java.io 下,大概有将近 80 个类,但是这些类大概可以