Tag Archives: 二进制

java序列化Serializable小结

作者: dplord, 访问量 439





一、什么是java序列化

首先谈一下什么是序列化, 序列化简单的来说,序列化会用在把一个对象、一个变量,以数据形式保留。比如把对象的二进制存在缓存、文件中。等需要的时候再次拿出来,反序列化为你想要的变量、对象。Java序列化即为,在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。

由上面可知,java序列化是为了保存一个对象当前属性的,便于还原重新构造这个对象。因此,java中的序列化做的即为保存java对象的成员变量的。因此,java序列化并不会关注类中的静态变量。同时java也对一些不需要序列化的变量提供了关键词transient,用transient修饰的变量就不会被序列化跟反序列化。

序列化好的数据,可以便于本地写入文件、或者写入redis缓存供多个机器通过读缓存的方式一起使用这个对象。用处很多。

二、java序列化的用法示例

首先写一个类implements  java.io.Serializable,如下

package serialize;

import java.io.Serializable;

/**
 * Created by dengpan on 2016/12/27.
 */
public class A1 implements Serializable {
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

这里简单的实现序列化一个对象到文件并从文件读出对象

package serialize;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
 * Created by dengpan on 2016/12/28.
 */
public class A1_2 {
    public static void main(String[] args) throws Exception {
        //序列化
        A1 obj = new A1();
        obj.setAge(12);
        obj.setName("nihao");

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/Users/dengpan/1.bin"));
        oos.writeObject(obj);
        oos.flush();
        oos.close();


        //反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/Users/dengpan/1.bin"));
        Object obj2 = ois.readObject();
        System.out.println(obj2.getClass());
        System.out.println(obj2.toString());
    }
}

即可正常的实现序列化到文件跟从文件反序列化到对象。

三、序列化原理

3.1 如何序列化一个对象

把一个对象从一个内存对象序列化为字节,如果让我做一个简单的序列化设计的话,即保存对象所有属性的值,对于int、short、float、boolean、byte、string各种类型,设定好相应的write、read方法。写的时候,按取到对象属性的顺序依次写入,读的时候按照对象属性的顺序依次独出。

下文3.3节会给出自己基于反射做的最简单的对象序列化。

 3.2 java序列化

序列化代码例子,java序列化的话,很简单。如果是把一个对象序列化的话,implement java.io.Serializable 接口即可。一个类如果是implements java.io.Serializable的话,它的子类也是可以序列化的。序列化的时候,如果一个成员变量是基本类型如int、double类型的,这些可以被序列化,但是如果成员变量的类型是未实现Serializable接口的类的话,需要把成员变量的类型这个类,实现序列化接口 Serializable。

3.3 实现Serializable接口的意义与自定义实现序列化过程

有人会问一个类仅仅implements Serializable,这个有什么意义呢。其实implements Serializable仅仅代表这个类可以被序列化。这个在下文会具体提到。如果你想自定义实现这个序列化、反序列化接口。那么在implements Serializable之后,复写writeObject、readObject即可。简单demo如下:

package serialize;

import java.io.IOException;
import java.io.Serializable;

/**
 * Created by dengpan on 2017/1/14.
 */
public class Person implements Serializable {
    private static final long serialVersionUID = 1L;

    private int age;
    private String name;

    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        //自定义序列化
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        //自定义反序列化
    }
}

3.4 序列化ID serialVersionUID的作用

java的serialVersionUID, 可以获取一个序列化ID,一个已经编译好的java class。序列化ID是已经决定的。这个序列化的ID用于我们做版本控制,比如我先写好了一个类的serialVersionUID = 1L,之后我把这个类serialVersionUID改为2L,这样之前序列化存下来的数据就因为serialVersionUID不一致,因此抛出InvalidClassException的异常。这样便于在一些传输序列化数据的过程中,做版本控制。这个serialVersionUID是会被写入文件序列化之后的字节里面去的。下文3.2.3会详细按照字节来解读序列化的过程与意义。

一般建议我们在一个实现了Serializable接口的类,手动写入serialVersionUID, 例如:private static final long serialVersionUID = 1L 当然这个1L是我自己写的,可以随便写入一个值。java IDE也提供了随机生成serialVersionUID的方法。这里介绍下我常用的jetbrains Idea 给一个类生成随机Serializable的方法。

 

QQ20170114-0@2x

 

在Serialization issues->Serializable class without “serialVersionUID”, 勾选此处。

在一个implements Serializable 接口的class里,光标停留在类名处,alt + enter。在弹出的框选择add “serialVersionUID” field。即可随机生成serialVersionUID。

QQ20170114-1@2x

 

也可用java官方自带的工具serialver,生成或者查看serialVersionUID。

用法为: serialver -classpath .  Person,即可查看或者生成serialVersionUID。可以看到输出 Person:    private static final long serialVersionUID = 1L;

3.5 序列化过程字节解读

比如一个类A1,定义如下:

package serialize;

import java.io.Serializable;

/**
 * Created by dengpan on 2016/12/27.
 */
public class A1 implements Serializable {
    private static final long serialVersionUID = 1L;
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

用如下代码序列化这个对象到1.bin这个文件

    //序列化
    A1 obj = new A1();
    obj.setName("王祖贤");
    obj.setAge(18);
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/Users/dengpan/tmp/1.bin"));
    oos.writeObject(obj);
    oos.flush();
    oos.close();

文件大小,文件字节信息如下图:

QQ20170114-5@2x

 

下面从序列化的顺序来分析下字节的含义与写入顺序。

这里给出ObjectOutputStream的writeObject的调用栈:

writeObject —> writeObject0 —>writeOrdinaryObject—>writeSerialData—>invokeWriteObject

上图可以看到, 序列化的对象是serialize.A1 的实例,该类有2个成员变量int age、String name,分别赋值为age=18,name=”王祖贤”。将这个对象序列化后字节为83b。

来分析下这个83b字节的含义。

① ac ed 00 05 只是写入序列化文件的一个固定标记,起标识作用。这个常量在接口ObjectStreamConstants定义如下:final static short STREAM_MAGIC = (short)0xaced;如下图红色框框表明。

QQ20170115-2@2x

这里到了到了java.io.ObjectOutputStream#writeObject0这一步,这个方法里面。有这么一段代码。

   // remaining cases
        if (obj instanceof String) {
        writeString((String) obj, unshared);
    } else if (cl.isArray()) {
        writeArray(obj, desc, unshared);
    } else if (obj instanceof Enum) {
        writeEnum((Enum<?>) obj, desc, unshared);
    } else if (obj instanceof Serializable) {
        writeOrdinaryObject(obj, desc, unshared);
    } else {
        if (extendedDebugInfo) {
            throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
        } else {
            throw new NotSerializableException(cl.getName());
        }
    }                     

可以看到序列化要检测obj是否是String、Array、Enum或者是实现了Serializable接口。因此implements Serializable是作为一个类可不可以被序列化的标记。

② 接下来的73 72 00 代表也是一种标识,在

73在java.io.ObjectOutputStream#writeOrdinaryObject  bout.writeByte(TC_OBJECT);这句写入。剩下的72 00 也是一种标志flag写入的。跟序列化到数据关系不大。

写入的是跟这个类相关的描述flag。

QQ20170115-3@2x

③然后开始调用writeNonProxyDesc 写类名了。类名调用的是out.writeUTF(name); 先写类名长度、再写内容。下图红色框框中的0c 是代表类名serialize.A1的长度为12,serialize.A1的16进制等于 73 65 72 69 61 6c 69 7a 65 2e 41 31。在下图绿色框框中表明。

QQ20170115-4@2x

④写完了类名,开始写序列序列化ID   serialVersionUID了。out.writeLong(getSerialVersionUID());讲序列化id作为一个long,8个字节写入。00 00 00 00 00 00 00 01就是我定义的 private static final long serialVersionUID = 1L;  从这里可以看到序列化id是写入文件的,反序列化的时候,也会读这个id,不一致就会抛异常。因此建议大家在要序列化的地方都指定一个序列化id。

⑤ 接下来开始写field了。写入成员变量数据与信息。下图红色框框的02,是检测该类是不是externalizable的,是一个bye的flag标志位。绿色框框的00 02,代表这个类有2个field。用short数字写入field长度。因此最多能定义65536个field,多了序列化写不进去。

QQ20170115-5@2x

其实java中, 根据编译之后的class文件有一个用无符号两个字节u2记录该class类有多少个显示声明的类或者字段,也就是2的16次方,65536个。跟这里设计是吻合的。

⑥ 写完field数量,开始写field数据了。下图中,红框中的49是写的代表第一个field age字段的类型I, 根据java.io.ObjectStreamField.getTypeCode() 得到。

QQ20170115-6@2x

java的各种typeCode列如下表:

B byte
C char
D double
F float
I int
J long
L class或者interface
S short
Z boolean
[ array

绿色框框的00 03,是一个short数字,代表field age的长度,61 67 65 就是字符串age的16进制表示。接下来的4c是L即第二个field name的typeCode,代表这个字段是一个Class、00 04代表是第二个field name的长度,6e 61 6d 65 是name的16进制表示。接下来下图中的74是bout.writeByte(TC_STRING);
bout.writeUTF(str, utflen);中的 TC_STRING,写入string的标识。00 12代表的是name的类型Ljava/lang/String; 的长度。前面的L代表是一个Class,有上表所示。接下来红框中的 18个字节代表的是 Ljava/lang/String; 这个字符串的16进制内容。

QQ20170115-14@2x

⑦ 类描述、以及field信息都写进去了,现在开始调用java.io.ObjectOutputStream#writeSerialData方法写序列化数据了。下图红色的78是bout.writeByte(TC_ENDBLOCKDATA); 写的一个block data分隔字节,是一个常量。接下来的绿色框框中的70, 是TC_BASE(112)的16进制表示。是代表接下来要写一个数字了。00 00 00 12是4个字节,标识的age = 18,这个18。74是TC_STRING的标识,代表接下来要写的是字符串。。00 09是1个short数字,占2个字节,代表字符串”王祖贤”占9个字节。紫红色的框中的e7 8e 8b e7 a5 96 e8 b4 a4代表字符串 “王祖贤” 的16进制表示。

QQ20170115-15@2x

四、java序列化小结

由以上分析可以看到,序列化中需要指定serialVersionUID,同时java序列化,是一个基本的序列化,也只能在java中保存一个对象到字节流,并不能很好的跟别的语言传输序列化的字节流做RPC用。基于java序列化的RMI并不能跟别的语言RPC,就是因为java序列化只是java语言内部用的。如果需要多语言间共享序列化数据或者追求更小体积、更好的序列化性能,可以选择thrift、protobuf等等序列化技术。

五、参考文章

 

二进制文件跟普通文本文件的区别

作者: dplord, 访问量 1071





任何文件都可以划分为二进制文件(binary file)跟文本文件(text file), 两种文件表面上看起来显示,但是两种文件编码数据的方式却有差异。两种文件都是用一系列的字节编码数据,在文本文件中,所编码的字节就是代表文本文件的内容,而二进制文件的编码,却代表自定义的数据格式,需要特殊的去decode文件内容。下面就用『ab12\n3』为代表写入两种文件,读取看看差异。(\n 是换行符)

写程序如下:

#include <stdio.h>
#include <string.h>

int main() {
	FILE *fp = fopen("data.text", "w+");
	FILE *fp1 = fopen("data.bin", "wb+");
	
	
	if((fp == NULL) || (fp1 == NULL)) 
	{
		fprintf(stderr, "can not open file...");
		return -1;
	}
		
	const char *str = "ab12\n3";
	int len = strlen(str);
	fwrite(str, len, 1, fp);
		
	const char *str1 = "ab";
	int a = 12;
	const char *str2 = "\n";
	int b = 3;
	int len1 = strlen(str1);
	int len2 = strlen(str2);
	
	fwrite(str1, len1, 1, fp1);
	fwrite(&a, 1, 1, fp1);
	fwrite(str2, len2, 1, fp1);
	fwrite(&b, 1, 1, fp1);
	
	fclose(fp);
	fclose(fp1);
	return 0;
}

查看文件大小,  如下

➜  ~ ll -h data.text data.bin
-rw-r--r--  1 dengpan  staff     5B  3 14 01:38 data.bin
-rw-r--r--  1 dengpan  staff     6B  3 14 01:38 data.text
➜  ~ hexdump data.text
0000000 61 62 31 32 0a 33
0000006
➜  ~ hexdump data.bin
0000000 61 62 0c 0a 03
0000005

二进制文件data.bin是5bit, 文本文件data.text是6bit。其中文本文件data.text中的6个bit,分别对应a、b、1、2、\n、3。其中二进制文件data.bin里面的5个bit分别对应a、b、12、\n、3。

其中data.text就是其中的文本字符串的anscii吗,一个字符一个字符对应的。具体个参照ascii-table。文本编辑器打开data.text会换行,是因为碰到了换行符0a,编辑器会自动做换行处理的。

二进制文件的每一个bit放什么数据完全可以自己控制,可以放int、short、char等等,也可以放struct数据。当时解析二进制file的时候,需要知道解析规则,不然也不可读。以下是data.bin根据写的顺序写的读出来输出内容的解析代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
	FILE *fp = fopen("data.bin", "rb+");
	if(fp == NULL) 
	{
		fprintf(stderr, "can not open file...");
		return -1;
	}
	
	int len_a = 2;
	char *a = malloc(len_a);
	int num1 = 0;
	int len_b = 1;
	char *b = malloc(len_b);
	int num2 = 0;
	
	fread(a, len_a, 1, fp);
	fread(&num1, 1, 1, fp);
	fread(b, len_b, 1, fp);
	fread(&num2, 1, 1, fp);

	printf("%s%d%s%d\n", a, num1, b, num2);
	fclose(fp);
	return 0;
}

其实文本文件本身就是一个特殊的binary file, 只不过是按照字符串内容,依次按字节写内容而已。二进制文件是按照自己的编码格式来的,常见的二进制文件比如图片、文档、视频,遵循一定的约定,通常是约定头部字节等于一些固定开头的值,各个文件约定也不尽相同,比如jpg的文件的头4个字节是固定的FF D8 FF E0 或者 FF D8 FF E1 或者 FF D8 FF E8, png的头8个字节是89 50 4E 47 0D 0A 1A 0A。用hexdump可以查看一个文件的hex内容。比如:

QQ20160314-1@2x

具体的查看每种文件的头部字节约定可以查看,File signatures网站