很多商业项目用到数据库、内存映射文件和普通文件来完成项目中的序列化处理的需求,但是这些方法很少会依靠于Java序列化。本文也不是用来解释序列化的,而是一起来看看面试中有关序列化的问题,这些问题你很有可能不了解。“Java序列化指的是将对象转换程字节格式并将对象状态保存在文件中,通常是.ser扩展名的文件。然后可以通过.ser文件重新创建Java对象,这个过程为返序列化”
Java序列化的API中提供了开发人员进行序列化对象的机制,通过Serializable和Externalizable接口。
一起看看这些问题:
1)Java中的Serializable接口和Externalizable接口有什么区别?
这个是面试中关于Java序列化问的最多的问题。我的回答是,Externalizable接口提供了两个方法writeExternal()和readExternal()。这两个方法给我们提供了灵活处理Java序列化的方法,通过实现这个接口中的两个方法进行对象序列化可以替代Java中默认的序列化方法。正确的实现Externalizable接口可以大幅度的提高应用程序的性能。
2)Serializable接口中有借个方法?如果没有方法的话,那么这么设计Serializable接口的目的是什么?
Serializable接口在java.lang包中,是Java序列化机制的核心组成部分。它里面没有包含任何方法,我们称这样的接口为标识接口。如果你的类实现了Serializable接口,这意味着你的类被打上了“可以进行序列化”的标签,并且也给了编译器指示,可以使用序列化机制对这个对象进行序列化处理。
3)什么是serialVersionUID?如果你没有定义serialVersionUID意味着什么?
SerialVersionUID应该是你的类中的一个publicstatic final类型的常量,如果你的类中没有定义的话,那么编译器将抛出警告。如果你的类中没有制定serialVersionUID,那么Java编译器会根据类的成员变量和一定的算法生成用来表达对象的serialVersionUID ,通常是用来表示类的哈希值(hash code)。结论是,如果你的类没有实现SerialVersionUID,那么如果你的类中如果加入或者改变成员变量,那么已经序列化的对象将无法反序列化。这是以为,类的成员变量的改变意味这编译器生成的SerialVersionUID的值不同。Java序列化过程是通过正确SerialVersionUID来对已经序列化的对象进行状态恢复。
4)当对象进行序列化的时候,如果你不希望你的成员变量进行序列化,你怎么办?
这个问题也会这么问,如何使用暂态类型的成员变量?暂态和静态成员变量是否会被序列化等等。如果你不希望你的对象中的成员变量的状态得以保存,你可以根据需求选择transient或者static类型的变量,这样的变量不参与Java序列化处理的过程。
5)如果一个类中的成员变量是其它符合类型的Java类,而这个类没有实现Serializable接口,那么当对象序列化的时候会怎样?
如果你的一个对象进行序列化,而这个对象中包含另外一个引用类型的成员编程,而这个引用的类没有实现Serializable接口,那么当对象进行序列化的时候会抛出“NotSerializableException“的运行时异常。
6)如果一个类是可序列化的,而他的超类没有,那么当进行反序列化的时候,那些从超类继承的实例变量的值是什么?
Java中的序列化处理实例变量只会在所有实现了Serializable接口的继承支路上展开。所以当一个类进行反序列化处理的时候,超类没有实现Serializable接口,那么从超类继承的实例变量会通过为实现序列化接口的超类的构造函数进行初始化。
7)Can you Customize Serialization process or can you override defaultSerialization process in Java?
7)你能够自定义序列化处理的代码吗或者你能重载Java中默认的序列化方法吗?
答案是肯定的,可以。我们都知道可以通过ObjectOutputStream中的writeObject()方法写入序列化对象,通过ObjectInputStream中的readObject()读入反序列化的对象。这些都是Java虚拟机提供给你的两个方法。如果你在你的类中定义了这两个方法,那么JVM就会用你的方法代替原有默认的序列化机制的方法。你可以通过这样的方式类自定义序列化和反序列化的行为。需要注意的一点是,最好将这两个方法定义为private,以防止他们被继承、重写和重载。也只有JVM可以访问到你的类中所有的私有方法,你不用担心方法私有不会被调用到,Java序列化过程会正常工作。
8)假设一个新的类的超类实现了Serializable接口,那么如何让这个新的子类不被序列化?
如果一个超类已经序列化了,那么无法通过是否实现什么接口的方式再避免序列化的过程了,但是也还有一种方式可以使用。那就是需要你在你的类中重新实现writeObject()和readObject()方法,并在方法实现中通过抛出NotSerializableException。
9)在Java进行序列化和反序列化处理的时候,哪些方法被使用了?
这个是面试中常见的问题,主要用来考察你是否对readObject()、writeObject()、readExternal()和writeExternal()方法的使用熟悉。Java序列化是通过java.io.ObjectOutputStream这个类来完成的。这个类是一个过滤器流,这个类完成对底层字节流的包装来进行序列化处理。我们通过ObjectOutputStream.writeObject(obj)进行序列化,通过ObjectInputStream.readObject()进行反序列化。对writeObject()方法的调用会触发Java中的序列化机制。readObject()方法用来将已经持久化的字节数据反向创建Java对象,该方法返回Object类型,需要强制转换成你需要的正确类型。
10)Suppose you have a class which you serialized it and stored in persistence andlater modified that class to add a new field. What will happen if youdeserialize the object already serialized?
10)假设你有一个类并且已经将这个类的某一个对象序列化存储了,那么如果你在这个类中加入了新的成员变量,那么在反序列化刚才那个已经存在的对象的时候会怎么样?
这个取决于这个类是否有serialVersionUID成员。通过上面的,我们已经知道如果你的类没有提供serialVersionUID,那么编译器会自动生成,而这个serialVersionUID就是对象的hash code值。那么如果加入新的成员变量,重新生成的serialVersionUID将和之前的不同,那么在进行反序列化的时候就会产生java.io.InvalidClassException的异常。这就是为什么要建议为你的代码加入serialVersionUID的原因所在了。
11)JAVA反序列化时会将NULL值变成""字符!!
在java中socket传输数据时,数据类型往往比较难选择。可能要考虑带宽、跨语言、版本的兼容等问题。比较常见的做法有两种:一是把对象包装成JSON字符串传输,二是采用java对象的序列化和反序列化。随着Google工具protoBuf的开源,protobuf也是个不错的选择。对JSON,Object Serialize,ProtoBuf 做个对比。
定义一个待传输的对象UserVo:
1. public class UserVo{
2. private String name;
3. private int age;
4. private long phone;
5.
6. private List<UserVo> friends;
7. ……
8. }
初始化UserVo的实例src:
1. UserVo src = new UserVo();
2. src.setName("Yaoming");
3. src.setAge(30);
4. src.setPhone(13789878978L);
5.
6. UserVo f1 = new UserVo();
7. f1.setName("tmac");
8. f1.setAge(32);
9. f1.setPhone(138999898989L);
10.UserVo f2 = new UserVo();
11.f2.setName("liuwei");
12.f2.setAge(29);
13.f2.setPhone(138999899989L);
14.
15.List<UserVo> friends = new ArrayList<UserVo>();
16.friends.add(f1);
17.friends.add(f2);
18.src.setFriends(friends);
JSON格式
采用Google的gson-2.2.2.jar进行转义
1. Gson gson = new Gson();
2. String json = gson.toJson(src);
得到的字符串:
1. {"name":"Yaoming","age":30,"phone":13789878978,"friends":[{"name":"tmac","age":32,"phone":138999898989},{"name":"liuwei","age":29,"phone":138999899989}]}
字节数为153
Json的优点:明文结构一目了然,可以跨语言,属性的增加减少对解析端影响较小。缺点:字节数过多,依赖于不同的第三方类库。
Object Serialize
UserVo实现Serializalbe接口,提供唯一的版本号:
1. public class UserVo implements Serializable{
2.
3. private static final long serialVersionUID = -5726374138698742258L;
4. private String name;
5. private int age;
6. private long phone;
7.
8. private List<UserVo> friends;
序列化方法:
1. ByteArrayOutputStream bos = new ByteArrayOutputStream();
2. ObjectOutputStream os = new ObjectOutputStream(bos);
3. os.writeObject(src);
4. os.flush();
5. os.close();
6. byte[] b = bos.toByteArray();
7. bos.close();
字节数是238
反序列化:
1. ObjectInputStream ois = new ObjectInputStream(fis);
2. vo = (UserVo) ois.readObject();
3. ois.close();
4. fis.close();
Object Serializalbe 优点:java原生支持,不需要提供第三方的类库,使用比较简单。缺点:无法跨语言,字节数占用比较大,某些情况下对于对象属性的变化比较敏感。
对象在进行序列化和反序列化的时候,必须实现Serializable接口,但并不强制声明唯一的serialVersionUID
是否声明serialVersionUID对于对象序列化的向上向下的兼容性有很大的影响。我们来做个测试:
思路一
把UserVo中的serialVersionUID去掉,序列化保存。反序列化的时候,增加或减少个字段,看是否成功。
1. public class UserVo implements Serializable{
2. private String name;
3. private int age;
4. private long phone;
5.
6. private List<UserVo> friends;
保存到文件中:
1. ByteArrayOutputStream bos = new ByteArrayOutputStream();
2. ObjectOutputStream os = new ObjectOutputStream(bos);
3. os.writeObject(src);
4. os.flush();
5. os.close();
6. byte[] b = bos.toByteArray();
7. bos.close();
8.
9. FileOutputStream fos = new FileOutputStream(dataFile);
10.fos.write(b);
11.fos.close();
增加或者减少字段后,从文件中读出来,反序列化:
1. FileInputStream fis = new FileInputStream(dataFile);
2. ObjectInputStream ois = new ObjectInputStream(fis);
3. vo = (UserVo) ois.readObject();
4. ois.close();
5. fis.close();
结果:抛出异常信息
1. Exception in thread "main" java.io.InvalidClassException: serialize.obj.UserVo; local class incompatible: stream classdesc serialVersionUID = 3305402508581390189, local class serialVersionUID = 7174371419787432394
2. at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:560)
3. at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1582)
4. at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1495)
5. at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1731)
6. at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1328)
7. at java.io.ObjectInputStream.readObject(ObjectInputStream.java:350)
8. at serialize.obj.ObjectSerialize.read(ObjectSerialize.java:74)
9. at serialize.obj.ObjectSerialize.main(ObjectSerialize.java:27)
思路二
eclipse指定生成一个serialVersionUID,序列化保存,修改字段后反序列化
略去代码
结果:反序列化成功
结论
如果没有明确指定serialVersionUID,序列化的时候会根据字段和特定的算法生成一个serialVersionUID,当属性有变化时这个id发生了变化,所以反序列化的时候就会失败。抛出“本地classd的唯一id和流中class的唯一id不匹配”。
jdk文档关于serialVersionUID的描述:
写道
如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值,如“Java(TM)对象序列化规范”中所述。不过,强烈建议所有可序列化类都显式声明serialVersionUID 值,原因是计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。因此,为保证serialVersionUID 值跨不同 java 编译器实现的一致性,序列化类必须声明一个明确的 serialVersionUID 值。还强烈建议使用private 修饰符显示声明 serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类 -- serialVersionUID 字段作为继承成员没有用处。数组类不能声明一个明确的 serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配 serialVersionUID 值的要求。
Google ProtoBuf
protocol buffers 是google内部得一种传输协议,目前项目已经开源(http://code.google.com/p/protobuf/)。它定义了一种紧凑得可扩展得二进制协议格式,适合网络传输,并且针对多个语言有不同得版本可供选择。
以protobuf-2.5.0rc1为例,准备工作:
下载源码,解压,编译,安装
1. tar zxvf protobuf-2.5.0rc1.tar.gz
2. ./configure
3. ./make
4. ./make install
测试:
1. MacBook-Air:~ ming$ protoc --version
2. libprotoc 2.5.0
安装成功!进入源码得java目录,用mvn工具编译生成所需得jar包,protobuf-java-2.5.0rc1.jar
1、编写.proto文件,命名UserVo.proto
1. package serialize;
2.
3. option java_package = "serialize";
4. option java_outer_classname="UserVoProtos";
5.
6. message UserVo{
7. optional string name = 1;
8. optional int32 age = 2;
9. optional int64 phone = 3;
10. repeated serialize.UserVo friends = 4;
11.}
2、在命令行利用protoc 工具生成builder类
1. protoc -IPATH=.proto文件所在得目录 --java_out=java文件的输出路径 .proto的名称
得到UserVoProtos类
3、编写序列化代码
1. UserVoProtos.UserVo.Builder builder = UserVoProtos.UserVo.newBuilder();
2. builder.setName("Yaoming");
3. builder.setAge(30);
4. builder.setPhone(13789878978L);
5.
6. UserVoProtos.UserVo.Builder builder1 = UserVoProtos.UserVo.newBuilder();
7. builder1.setName("tmac");
8. builder1.setAge(32);
9. builder1.setPhone(138999898989L);
10.
11.UserVoProtos.UserVo.Builder builder2 = UserVoProtos.UserVo.newBuilder();
12.builder2.setName("liuwei");
13.builder2.setAge(29);
14.builder2.setPhone(138999899989L);
15.
16.builder.addFriends(builder1);
17.builder.addFriends(builder2);
18.
19.UserVoProtos.UserVo vo = builder.build();
20.
21.byte[] v = vo.toByteArray();
字节数53
4、反序列化
1. UserVoProtos.UserVo uvo = UserVoProtos.UserVo.parseFrom(dstb);
2. System.out.println(uvo.getFriends(0).getName());
结果:tmac,反序列化成功
google protobuf 优点:字节数很小,适合网络传输节省io,跨语言 。缺点:需要依赖于工具生成代码。
工作机制
proto文件是对数据的一个描述,包括字段名称,类型,字节中的位置。protoc工具读取proto文件生成对应builder代码的类库。protoc xxxxx --java_out=xxxxxx 生成java类库。builder类根据自己的算法把数据序列化成字节流,或者把字节流根据反射的原理反序列化成对象。官方的示例:https://developers.google.com/protocol-buffers/docs/javatutorial。
proto文件中的字段类型和java中的对应关系:
详见:https://developers.google.com/protocol-buffers/docs/proto
.proto Type | java Type | c++ Type |
double | double | double |
float | float | float |
int32 | int | int32 |
int64 | long | int64 |
uint32 | int | uint32 |
unint64 | long | uint64 |
sint32 | int | int32 |
sint64 | long | int64 |
fixed32 | int | uint32 |
fixed64 | long | uint64 |
sfixed32 | int | int32 |
sfixed64 | long | int64 |
bool | boolean | bool |
string | String | string |
bytes | byte | string |
字段属性的描述:
写道
required: a well-formed message must haveexactly one of this field.
optional: a well-formed message can have zero or one of this field (but notmore than one).
repeated: this field can be repeated any number of times (including zero) in awell-formed message. The order of the repeated values will be preserved.
protobuf 在序列化和反序列化的时候,是依赖于.proto文件生成的builder类完成,字段的变化如果不表现在.proto文件中就不会影响反序列化,比较适合字段变化的情况。做个测试:
把UserVo序列化到文件中:
1. UserVoProtos.UserVo vo = builder.build();
2. byte[] v = vo.toByteArray();
3. FileOutputStream fos = new FileOutputStream(dataFile);
4. fos.write(vo.toByteArray());
5. fos.close();
为UserVo增加字段,对应的.proto文件:
1. package serialize;
2.
3. option java_package = "serialize";
4. option java_outer_classname="UserVoProtos";
5.
6. message UserVo{
7. optional string name = 1;
8. optional int32 age = 2;
9. optional int64 phone = 3;
10. repeated serialize.UserVo friends = 4;
11. optional string address = 5;
12.}
从文件中反序列化回来:
1. FileInputStream fis = new FileInputStream(dataFile);
2. byte[] dstb = new byte[fis.available()];
3. for(int i=0;i<dstb.length;i++){
4. dstb[i] = (byte)fis.read();
5. }
6. fis.close();
7. UserVoProtos.UserVo uvo = UserVoProtos.UserVo.parseFrom(dstb);
8. System.out.println(uvo.getFriends(0).getName());
成功得到结果。
三种方式对比传输同样的数据,google protobuf只有53个字节是最少的。结论:
方式 | 优点 | 缺点 |
JSON | 跨语言、格式清晰一目了然 | 字节数比较大,需要第三方类库 |
Object Serialize | java原生方法不依赖外部类库 | 字节数比较大,不能跨语言 |
Google protobuf | 跨语言、字节数比较少 | 编写.proto配置用protoc工具生成对应的代码 |
序列化是指将一个对象序列化成字节流,便于存储或者网络传输;而反序列化恰好相反,将字节流,变回一个对象.我们平常用的比较多的是hessian序列化方式和java序列化方式,两种序列化方式的效率,以及序列化大小是不一样的,从测试结果看,hessian好一点.下面写了一个hessian序列化示例(没有文件IO与网络IO,纯粹序列化与反序列化):
/**
* 纯hessian序列化
* @param object
* @return
* @throws Exception
*/
public static byte[] serialize(Object object) throws Exception{
if(object==null){
throw new NullPointerException();
}
ByteArrayOutputStream os = new ByteArrayOutputStream();
HessianSerializerOutput hessianOutput=new HessianSerializerOutput(os);
hessianOutput.writeObject(object);
return os.toByteArray();
}
/**
* 纯hessian反序列化
* @param bytes
* @return
* @throws Exception
*/
public static Object deserialize(byte[] bytes) throws Exception{
if(bytes==null){
throw new NullPointerException();
}
ByteArrayInputStream is = new ByteArrayInputStream(bytes);
HessianSerializerInput hessianInput=new HessianSerializerInput(is);
Object object = hessianInput.readObject();
return object;
}
另外一种常见的是java序列化方式:
/**
* java序列化
* @param obj
* @return
* @throws Exception
*/
public static byte[] serialize(Object obj) throws Exception {
if (obj == null)
throw new NullPointerException();
ByteArrayOutputStream os = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(os);
out.writeObject(obj);
return os.toByteArray();
}
/**
* java反序列化
* @param by
* @return
* @throws Exception
*/
public static Object deserialize(byte[] by) throws Exception {
if (by == null)
throw new NullPointerException();
ByteArrayInputStream is = new ByteArrayInputStream(by);
ObjectInputStream in = new ObjectInputStream(is);
return in.readObject();
}
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
把Java对象转换为字节序列的过程称为对象的序列化。
把字节序列恢复为Java对象的过程称为对象的反序列化。
对象的序列化主要有两种用途:
1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
2) 在网络上传送对象的字节序列。
一. JDK类库中的序列化API
java.io.ObjectOutputStream代表对象输出流,它的writeObject(Objectobj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。、
只有实现了Serializable和Externalizable接口的类的对象才能被序列化。Externalizable接口继承自Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式 。
对象序列化包括如下步骤:
1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
2) 通过对象输出流的writeObject()方法写对象。
对象反序列化的步骤如下:
1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
2) 通过对象输入流的readObject()方法读取对象。
下面让我们来看一个对应的例子,类的内容如下:
import java.io.*;
import java.util.Date;
/**
* 对象的序列化和反序列化测试类.
* @author <ahref="mailto:xiexingxing1121@126.com">AmigoXie</a>
* @version 1.0
* Creation date: 2007-9-15- 下午21:45:48
*/
public class ObjectSaver {
/**
* @param args
* @author <ahref="mailto:xiexingxing1121@126.com">AmigoXie</a>
* Creation date: 2007-9-15 - 下午21:45:37
*/
public static void main(String[] args) throws Exception {
ObjectOutputStream out = new ObjectOutputStream
(new FileOutputStream("D:""objectFile.obj"));
//序列化对象
Customer customer = new Customer("阿蜜果",24);
out.writeObject("你好!");
out.writeObject(new Date());
out.writeObject(customer);
out.writeInt(123); //写入基本类型数据
out.close();
//反序列化对象
ObjectInputStream in = new ObjectInputStream
(new FileInputStream("D:""objectFile.obj"));
System.out.println("obj1=" + (String) in.readObject());
System.out.println("obj2=" + (Date) in.readObject());
Customer obj3 = (Customer) in.readObject();
System.out.println("obj3=" + obj3);
int obj4 = in.readInt();
System.out.println("obj4=" + obj4);
in.close();
}
}
class Customer implementsSerializable {
private String name;
private int age;
public Customer(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return "name=" + name + ", age=" + age;
}
}
输出结果如下:
obj1=你好!
obj2=Sat Sep 15 22:02:21 CST 2007
obj3=name=阿蜜果,age=24
obj4=123
因此例比较简单,在此不再详述。
二.实现Serializable接口
ObjectOutputStream只能对Serializable接口的类的对象进行序列化。默认情况下,ObjectOutputStream按照默认方式序列化,这种序列化方式仅仅对对象的非transient的实例变量进行序列化,而不会序列化对象的transient的实例变量,也不会序列化静态变量。
当ObjectOutputStream按照默认方式反序列化时,具有如下特点:
1) 如果在内存中对象所属的类还没有被加载,那么会先加载并初始化这个类。如果在classpath中不存在相应的类文件,那么会抛出ClassNotFoundException;
2) 在反序列化时不会调用类的任何构造方法。
如果用户希望控制类的序列化方式,可以在可序列化类中提供以下形式的writeObject()和readObject()方法。
privatevoid writeObject(java.io.ObjectOutputStream out) throws IOException
privatevoid readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException;
当ObjectOutputStream对一个Customer对象进行序列化时,如果该对象具有writeObject()方法,那么就会执行这一方法,否则就按默认方式序列化。在该对象的writeObjectt()方法中,可以先调用ObjectOutputStream的defaultWriteObject()方法,使得对象输出流先执行默认的序列化操作。同理可得出反序列化的情况,不过这次是defaultReadObject()方法。
有些对象中包含一些敏感信息,这些信息不宜对外公开。如果按照默认方式对它们序列化,那么它们的序列化数据在网络上传输时,可能会被不法份子窃取。对于这类信息,可以对它们进行加密后再序列化,在反序列化时则需要解密,再恢复为原来的信息。
默认的序列化方式会序列化整个对象图,这需要递归遍历对象图。如果对象图很复杂,递归遍历操作需要消耗很多的空间和时间,它的内部数据结构为双向列表。
在应用时,如果对某些成员变量都改为transient类型,将节省空间和时间,提高序列化的性能。
三. 实现Externalizable接口
Externalizable接口继承自Serializable接口,如果一个类实现了Externalizable接口,那么将完全由这个类控制自身的序列化行为。Externalizable接口声明了两个方法:
publicvoid writeExternal(ObjectOutput out) throws IOException
publicvoid readExternal(ObjectInput in) throws IOException , ClassNotFoundException
前者负责序列化操作,后者负责反序列化操作。
在对实现了Externalizable接口的类的对象进行反序列化时,会先调用类的不带参数的构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException:no valid constructor异常。
四. 可序列化类的不同版本的序列化兼容性
凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:
private static final longserialVersionUID;
以上serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的。如果对类的源代码作了修改,再重新编译,新生成的类文件的serialVersionUID的取值有可能也会发生变化。
类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的serialVersionUID,也有可能相同。为了提高哦啊serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示的定义serialVersionUID,为它赋予明确的值。显式地定义serialVersionUID有两种用途:
1) 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
2) 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。
Java序列化算法透析
Serialization(序列化)是一种将对象以一连串的字节描述的过程;反序列化deserialization是一种将这些字节重建成一个对象的过程。Java序列化API提供一种处理对象序列化的标准机制。在这里你能学到如何序列化一个对象,什么时候需要序列化以及Java序列化的算法,我们用一个实例来示范序列化以后的字节是如何描述一个对象的信息的。
序列化的必要性
Java中,一切都是对象,在分布式环境中经常需要将Object从这一端网络或设备传递到另一端。
这就需要有一种可以在两端传输数据的协议。Java序列化机制就是为了解决这个问题而产生。
如何序列化一个对象
一个对象能够序列化的前提是实现Serializable接口,Serializable接口没有方法,更像是个标记。
有了这个标记的Class就能被序列化机制处理。
import java.io.Serializable;
class TestSerial implements Serializable {
public byte version = 100;
public byte count = 0;
}
然后我们写个程序将对象序列化并输出。ObjectOutputStream能把Object输出成Byte流。
我们将Byte流暂时存储到temp.out文件里。
public static void main(String args[]) throwsIOException {
FileOutputStream fos = newFileOutputStream("temp.out");
ObjectOutputStream oos = newObjectOutputStream(fos);
TestSerial ts = new TestSerial();
oos.writeObject(ts);
oos.flush();
oos.close();
}
如果要从持久的文件中读取Bytes重建对象,我们可以使用ObjectInputStream。
public staticvoid main(String args[]) throws IOException {
FileInputStream fis = newFileInputStream("temp.out");
ObjectInputStream oin = newObjectInputStream(fis);
TestSerial ts = (TestSerial) oin.readObject();
System.out.println("version="+ts.version);
}
执行结果为 100.
对象的序列化格式
将一个对象序列化后是什么样子呢?打开刚才我们将对象序列化输出的temp.out文件
以16进制方式显示。内容应该如下:
AC ED 00 05 73 72 00 0A 53 65 72 69 61 6C 54 65
73 74 A0 0C 34 00 FE B1 DD F9 02 00 02 42 00 05
63 6F 75 6E 74 42 00 07 76 65 72 73 69 6F 6E 78
70 00 64
这一坨字节就是用来描述序列化以后的TestSerial对象的,我们注意到TestSerial类中只有两个域:
public byte version = 100;
public byte count = 0;
且都是byte型,理论上存储这两个域只需要2个byte,但是实际上temp.out占据空间为51bytes,也就是说除了数据以外,还包括了对序列化对象的其他描述
Java的序列化算法
序列化算法一般会按步骤做如下事情:
◆将对象实例相关的类元数据输出。
◆递归地输出类的超类描述直到不再有超类。
◆类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
◆从上至下递归输出实例的数据
我们用另一个更完整覆盖所有可能出现的情况的例子来说明:
class parentimplements Serializable {
int parentVersion = 10;
}
class containimplements Serializable{
Int containVersion = 11;
}
public classSerialTest extends parent implements Serializable {
int version = 66;
contain con = new contain();
public int getVersion() {
return version;
}
public static void main(Stringargs[]) throws IOException {
FileOutputStream fos = newFileOutputStream("temp.out");
ObjectOutputStream oos =new ObjectOutputStream(fos);
SerialTest st = newSerialTest();
oos.writeObject(st);
oos.flush();
oos.close();
}
}
·
1. AC ED: STREAM_MAGIC. 声明使用了序列化协议.
2. 00 05: STREAM_VERSION. 序列化协议版本.
3. 0x73: TC_OBJECT. 声明这是一个新的对象.
4. 0x72: TC_CLASSDESC. 声明这里开始一个新Class。
5. 00 0A: Class名字的长度.
6. 53 65 72 69 61 6c 54 65 73 74: SerialTest,Class类名.
7. 05 52 81 5A AC 66 02 F6: SerialVersionUID, 序列化ID,如果没有指定,
则会由算法随机生成一个8byte的ID.
8. 0x02: 标记号. 该值声明该对象支持序列化。
9. 00 02: 该类所包含的域个数。
10.0x49: 域类型. 49 代表"I", 也就是Int.
11.00 07: 域名字的长度.
12.76 65 72 73 69 6F 6E: version,域名字描述.
13.0x4C: 域的类型.
14.00 03: 域名字长度.
15.63 6F 6E: 域名字描述,con
16.0x74: TC_STRING. 代表一个new String.用String来引用对象。
17.00 09: 该String长度.
18.4C 63 6F 6E 74 61 69 6E 3B: Lcontain;, JVM的标准对象签名表示法.
19.0x78: TC_ENDBLOCKDATA,对象数据块结束的标志
20.0x72: TC_CLASSDESC. 声明这个是个新类.
21.00 06: 类名长度.
22.70 61 72 65 6E 74: parent,类名描述。
23.0E DB D2 BD 85 EE 63 7A: SerialVersionUID, 序列化ID.
24.0x02: 标记号. 该值声明该对象支持序列化.
25.00 01: 类中域的个数.
26.0x49: 域类型. 49 代表"I", 也就是Int.
27.00 0D: 域名字长度.
28.70 61 72 65 6E 74 56 65 72 73 69 6F6E: parentVersion,域名字描述。
29.0x78: TC_ENDBLOCKDATA,对象块结束的标志。
30.0x70: TC_NULL, 说明没有其他超类的标志。.
31.00 00 00 0A: 10, parentVersion域的值.
32.00 00 00 42: 66, version域的值.
33.0x73: TC_OBJECT, 声明这是一个新的对象.
34.0x72: TC_CLASSDESC声明这里开始一个新Class.
35.00 07: 类名的长度.
36.63 6F 6E 74 61 69 6E: contain,类名描述.
37.FC BB E6 0E FB CB 60 C7: SerialVersionUID, 序列化ID.
38.0x02: Various flags. 标记号. 该值声明该对象支持序列化
39.00 01: 类内的域个数。
40.0x49: 域类型. 49 代表"I", 也就是Int..
41.00 0E: 域名字长度.
42.63 6F 6E 74 61 69 6E 56 65 72 73 69 6F6E: containVersion, 域名字描述.
43.0x78: TC_ENDBLOCKDATA对象块结束的标志.
44.0x70:TC_NULL,没有超类了。
45.00 00 00 0B: 11, containVersion的值.
这个例子是相当的直白啦。SerialTest类实现了Parent超类,内部还持有一个Container对象。
序列化后的格式如下:
ACED 00 05 73 72 00 0A 53 65 72 69 61 6C 54 65
7374 05 52 81 5A AC 66 02 F6 02 00 02 49 00 07
7665 72 73 69 6F 6E 4C 00 03 63 6F6E 74 00 09
4C 636F 6E 74 61 69 6E 3B 78 72 00 06 70 61 72
656E 74 0E DB D2 BD 85 EE 63 7A 02 00 01 49 00
0D70 61 72 65 6E 74 56 65 72 73 69 6F 6E 78 70
00 00 00 0A00 00 00 42 73 72 00 07 63 6F 6E74
6169 6E FC BB E6 0E FB CB 60 C7 02 00 01 49 00
0E63 6F 6E 74 61 69 6E 56 65 72 73 69 6F 6E 78
70 00 00 00 0B
我们来仔细看看这些字节都代表了啥。开头部分,见颜色:
序列化算法的第一步就是输出对象相关类的描述。例子所示对象为SerialTest类实例,
因此接下来输出SerialTest类的描述。见颜色:
接下来,算法输出其中的一个域,int version=66;见颜色:
然后,算法输出下一个域,contain con = new contain();这个有点特殊,是个对象。
描述对象类型引用时需要使用JVM的标准对象签名表示法,见颜色:
.接下来算法就会输出超类也就是Parent类描述了,见颜色:
下一步,输出parent类的域描述,int parentVersion=100;同见颜色:
到此为止,算法已经对所有的类的描述都做了输出。下一步就是把实例对象的实际值输出了。这时候是从parent Class的域开始的,见颜色:
还有SerialTest类的域:
再往后的bytes比较有意思,算法需要描述contain类的信息,要记住,
现在还没有对contain类进行过描述,见颜色:
.输出contain的唯一的域描述,int containVersion=11;
这时,序列化算法会检查contain是否有超类,如果有的话会接着输出。
最后,将contain类实际域值输出。
OK,我们讨论了java序列化的机制和原理,希望能对同学们有所帮助。
转自 http://www.java3z.com/cwbwebhome/article/article8/862.html
serialVersionUID值的重要作用
根据上面的分析,可以发现如果一个类可序列化,serialVersionUID建议给一个确定的值,不要由系统自动生成,否则在增减字段(不能修改字段类型及长度)时,如果两边的类的版本不同会导致反序列化失败.
注意问题
如果序列化时代码这样写:
SerialTest st = new SerialTest();
oos.writeObject((parent)st);
会发现序列化的对象依然是SerialTest,如果在分布式环境中用Parent反序列化(调用段不存在SerialTest),会造成ClassNotFoundException.
有关Java对象的序列化和反序列化也算是Java基础的一部分,下面对Java序列化的机制和原理进行一些介绍。
Java序列化算法透析
Serialization(序列化)是一种将对象以一连串的字节描述的过程;反序列化deserialization是一种将这些字节重建成一个对象的过程。Java序列化API提供一种处理对象序列化的标准机制。在这里你能学到如何序列化一个对象,什么时候需要序列化以及Java序列化的算法,我们用一个实例来示范序列化以后的字节是如何描述一个对象的信息的。
序列化的必要性
Java中,一切都是对象,在分布式环境中经常需要将Object从这一端网络或设备传递到另一端。这就需要有一种可以在两端传输数据的协议。Java序列化机制就是为了解决这个问题而产生。
如何序列化一个对象
一个对象能够序列化的前提是实现Serializable接口,Serializable接口没有方法,更像是个标记。有了这个标记的Class就能被序列化机制处理。
1. import java.io.Serializable;
2.
3. class TestSerial implements Serializable {
4.
5. public byte version = 100;
6.
7. public byte count = 0;
8.
9. }
然后我们写个程序将对象序列化并输出。ObjectOutputStream能把Object输出成Byte流。我们将Byte流暂时存储到temp.out文件里。
1. public static void main(String args[]) throws IOException {
2.
3. FileOutputStream fos = new FileOutputStream("temp.out");
4.
5. ObjectOutputStream oos = new ObjectOutputStream(fos);
6.
7. TestSerial ts = new TestSerial();
8.
9. oos.writeObject(ts);
10.
11. oos.flush();
12.
13. oos.close();
14.
15. }
如果要从持久的文件中读取Bytes重建对象,我们可以使用ObjectInputStream。
1. public static void main(String args[]) throws IOException {
2.
3. FileInputStream fis = new FileInputStream("temp.out");
4.
5. ObjectInputStream oin = new ObjectInputStream(fis);
6.
7. TestSerial ts = (TestSerial) oin.readObject();
8.
9. System.out.println("version="+ts.version);
10.
11. }
执行结果为
100.
对象的序列化格式
将一个对象序列化后是什么样子呢?打开刚才我们将对象序列化输出的temp.out文件,以16进制方式显示。内容应该如下:
AC ED 00 05 73 72 00 0A53 65 72 69 61 6C 54 65
73 74 A0 0C 34 00 FE B1DD F9 02 00 02 42 00 05
63 6F 75 6E 74 42 00 0776 65 72 73 69 6F 6E 78
70 00 64
这一坨字节就是用来描述序列化以后的
TestSerial对象的,我们注意到TestSerial类中只有两个域:
public byte version = 100;
public byte count = 0;
且都是byte型,理论上存储这两个域只需要2个byte,但是实际上temp.out占据空间为51bytes,也就是说除了数据以外,还包括了对序列化对象的其他描述。
Java的序列化算法
序列化算法一般会按步骤做如下事情:
◆将对象实例相关的类元数据输出。
◆递归地输出类的超类描述直到不再有超类。
◆类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
◆从上至下递归输出实例的数据
我们用另一个更完整覆盖所有可能出现的情况的例子来说明:
1. class parent implements Serializable {
2.
3. int parentVersion = 10;
4.
5. }
6.
7.
8.
9. class contain implements Serializable{
10.
11. int containVersion = 11;
12.
13. }
14.
15. public class SerialTest extends parent implements Serializable {
16.
17. int version = 66;
18.
19. contain con = new contain();
20.
21.
22.
23. public int getVersion() {
24.
25. return version;
26.
27. }
28.
29. public static void main(String args[]) throws IOException {
30.
31. FileOutputStream fos = new FileOutputStream("temp.out");
32.
33. ObjectOutputStream oos = new ObjectOutputStream(fos);
34.
35. SerialTest st = new SerialTest();
36.
37. oos.writeObject(st);
38.
39. oos.flush();
40.
41. oos.close();
42.
43. }
44.
45. }
这个例子是相当的直白啦。SerialTest类实现了Parent超类,内部还持有一个Container对象。
序列化后的格式如下:
AC ED 00 05 73 72 00 0A 53 65 72 69 616C 54 65
73 74 05 52 81 5A AC 66 02F6 02 00 02 4900 07
76 65 72 73 69 6F 6E 4C 00 03 63 6F 6E 74 00 09
4C 63 6F 6E 74 61 696E 3B 78 7200 06 70 61 72
65 6E 74 0E DB D2 BD 85 EE63 7A 02 00 01 49 00
0D 70 61 72 65 6E 74 56 6572 73 69 6F 6E 78 70
00 00 00 0A00 00 00 42 73 72 00 07 63 6F 6E 74
61 69 6E FC BB E6 0E FB CB60 C7 02 00 01 49 00
0E 63 6F 6E 74 61 69 6E 5665 72 73 69 6F 6E 78
70 00 00 00 0B
我们来仔细看看这些字节都代表了啥。开头部分,见颜色:
1. AC ED:STREAM_MAGIC. 声明使用了序列化协议.
2. 00 05:STREAM_VERSION. 序列化协议版本.
3. 0x73:TC_OBJECT. 声明这是一个新的对象.
序列化算法的第一步就是输出对象相关类的描述。例子所示对象为SerialTest类实例,因此接下来输出SerialTest类的描述。见颜色:
1. 0x72: TC_CLASSDESC. 声明这里开始一个新Class。
2. 00 0A: Class名字的长度.
3. 53 65 72 69 61 6c 5465 73 74: SerialTest,Class类名.
4. 05 52 81 5A AC 66 02F6: SerialVersionUID, 序列化ID,如果没有指定,则会由算法随机生成一个8byte的ID.
5. 0x02: 标记号. 该值声明该对象支持序列化。
6. 00 02: 该类所包含的域个数。
接下来,算法输出其中的一个域,int version=66;见颜色:
1. 0x49: 域类型.49 代表"I", 也就是Int.
2. 00 07: 域名字的长度.
3. 76 65 72 73 69 6F 6E:version,域名字描述.
然后,算法输出下一个域,contain con = new contain();这个有点特殊,是个对象。描述对象类型引用时需要使用JVM的标准对象签名表示法,见颜色:
1. 0x4C: 域的类型.
2. 00 03: 域名字长度.
3. 63 6F 6E: 域名字描述,con
4. 0x74:TC_STRING. 代表一个new String.用String来引用对象。
5. 00 09: 该String长度.
6. 4C 63 6F 6E 74 61 696E 3B: Lcontain;, JVM的标准对象签名表示法.
7. 0x78:TC_ENDBLOCKDATA,对象数据块结束的标志
.接下来算法就会输出超类也就是Parent类描述了,见颜色:
1. 0x72:TC_CLASSDESC. 声明这个是个新类.
2. 00 06: 类名长度.
3. 70 61 72 65 6E 74:parent,类名描述。
4. 0E DB D2 BD 85 EE 637A: SerialVersionUID, 序列化ID.
5. 0x02: 标记号. 该值声明该对象支持序列化.
6. 00 01: 类中域的个数.
下一步,输出parent类的域描述,int parentVersion=100;同见颜色:
1. 0x49: 域类型.49 代表"I", 也就是Int.
2. 00 0D: 域名字长度.
3. 70 61 72 65 6E 74 5665 72 73 69 6F 6E: parentVersion,域名字描述。
4. 0x78:TC_ENDBLOCKDATA,对象块结束的标志。
5. 0x70: TC_NULL, 说明没有其他超类的标志。.
到此为止,算法已经对所有的类的描述都做了输出。下一步就是把实例对象的实际值输出了。这时候是从parent Class的域开始的,见颜色:
1. 00 00 00 0A:10, parentVersion域的值.
还有SerialTest类的域:
1. 00 00 00 42:66, version域的值.
再往后的bytes比较有意思,算法需要描述contain类的信息,要记住,现在还没有对contain类进行过描述,见颜色:
1. 0x73:TC_OBJECT, 声明这是一个新的对象.
2. 0x72: TC_CLASSDESC声明这里开始一个新Class.
3. 00 07: 类名的长度.
4. 63 6F 6E 74 61 69 6E:contain,类名描述.
5. FC BB E6 0E FB CB 60C7: SerialVersionUID, 序列化ID.
6. 0x02: Variousflags. 标记号. 该值声明该对象支持序列化
7. 00 01: 类内的域个数。
.输出contain的唯一的域描述,int containVersion=11;
1. 0x49: 域类型.49 代表"I", 也就是Int..
2. 00 0E: 域名字长度.
3. 63 6F 6E 74 61 69 6E56 65 72 73 69 6F 6E: containVersion, 域名字描述.
4. 0x78: TC_ENDBLOCKDATA对象块结束的标志.
这时,序列化算法会检查contain是否有超类,如果有的话会接着输出。
1. 0x70:TC_NULL,没有超类了。
最后,将contain类实际域值输出。
1. 00 00 00 0B:11, containVersion的值.