对象序列化是一个用于将对象状态转换为字节流的过程,可以将其保存到磁盘文件中或通过网络发送到任何其他程序;从字节流创建对象的相反的过程称为反序列化。
目录
Serializable结合readObject()实现序列化
一.jdk自带的序列化和反序列化方式
Serializable实现序列化
在java中,可以通过输入输出流来进行对象的序列化和反序列化(ObjectInputStream和ObjectOutputStream),想要对一个对象进行序列化和反序列化,就要求对象实现java.io.Serializable接口,下面是一个简单的例子
/**
* @Description
* @Author chenpp
* @Date 2019/7/30 15:16
*/
public class JavaSerializer implements ISerializer {
//序列化使用的是对象输出流 把一个obj对象进行序列化,获得的字节序列写入到目标输出流中
public <T> byte[] serialize(T obj) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[0];
}
public <T> T deserialize(byte[] data, Class<T> clazz) {
ByteArrayInputStream bis = new ByteArrayInputStream(data);
try {
ObjectInputStream ois = new ObjectInputStream(bis);
return (T)ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
public class SerializerTest {
public static void main(String[] args) {
User user = new User("chenpp",21);
ISerializer javaSerializer = new JavaSerializer();
byte[] javaByte = javaSerializer.serialize(user);
User javaDe = javaSerializer.deserialize(javaByte, User.class);
System.out.println("==================java========================");
System.out.println("java ObjectOutputStream 序列化后的byte[]的length: "+javaByte.length);
System.out.println("oos反序列化:"+javaDe);
System.out.println();
}
/**
* @Description
* @Author chenpp
* @Date 2019/7/30 15:25
*/
public class User implements Serializable{
public User(){ }
public User(String name, int age ) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
什么是serialVersionUID?
对于使用Serializable接口实现序列化,有的IDE会要求开发者添加一个SerialVersionUID,否则就会有黄色的Warning警告,这是因为
如果没有设置serialVersionUID,那么在编译的时候会根据当前class(类名,接口名,成员方法和属性等)进行一个摘要算法,自动生成一个serialVersionUID唯一标识,当对应的class类发生改动(注释/空格/换行不算),会导致计算出来的serialVersionUID不一样,这样在进行反序列化进行验证的时候就会出现InvalidClassException异常
所以对于实现Serializable接口的序列化对象,往往都需要生成一个唯一固定的serialVersionUID,以保证在后续修改过程中不会出现由于对象属性等的变动导致原来被序列化的对象无法反序列化
Transient 关键字
transient修饰符仅适用于变量,不适用于方法和类。在序列化时,如果我们不想序列化特定变量以满足安全约束,那么我们应该将该变量声明为transient。执行序列化时,JVM会忽略transient变量的原始值并将默认值保存到文件中。因此,transient意味着不要序列化。
Externalizable实现序列化
除了上述Serializable接口,开发还可以通过实现Externalizable接口来满足序列化的要求
其特点:
1.与Serializable接口一样,推荐使用一个固定的serialVersionUID
2.通过在writeExternal()和readExternal()方法里手动序列化和反序列化相关的字段来实现
3.由于所有字段是否序列化都是通过writeExternal()方法来控制的,故其序列化的字段不受transient关键字的影响
如下:
/**
* @Description
* @Author chenpp
* @Date 2019/7/30 15:25
*/
public class UserExt implements Externalizable {
private static final long serialVersionUID = 2717235404652779769L;
//用户姓名
private String name;
private int age;
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(this.name);
out.writeInt(this.age);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = (String) in.readObject();
this.age = in.readInt();
}
public UserExt(){ }
public UserExt(String name, int age ) {
this.name = name;
this.age = age;
}
}
上述两种序列化方式:使用Serializable接口会更加简单,接口会帮我们默认序列化所有字段,而使用Externalizable接口会更加灵活,开发者可以自己控制想要序列化的所有字段,但代码相对会比较繁琐
Serializable还有一种方式可以结合两种接口的优点
Serializable结合readObject()实现序列化
将一个对象实现Serializable,同时添加readObject和writeObejct方法,这样在对对象进行序列化的时候会调用readObject和writeObject方法,对于这个方法的定义有如下三个要求:
1.方法必须要被private修饰 ----->才能被调用
2.第一行调用默认的defaultRead/WriteObject(); ----->隐式序列化非static和transient
3.调用read/writeObject()将获得的值赋给相应的值 --->显式序列化
这样一来,既可以默认序列化所有的非static和transient字段,又可以灵活的序列化开发想要的字段
其原因是因为在ObjectOutputStream调用writeObject进行序列化的时候通过反射调用了对象的writeObject()方法
在jdk的源码里有很多地方使用到了类似的方法,比如:ArrayList,HashMap等
ArrayList里实际存储数组元素的Obejct[] elementData就被transient关键字修饰,然后通过writeObject对象,在序列化的时候序列化elementData里有效的元素(因为elementData的数组大小是大于list的元素个数的)和个数
二.其他常见的序列化和反序列方法
除了上述的jdk字段的ObjectInputStream,ObjectOutputStream的序列化方式,目前常用的还有JSON,XML,Hessian
XML曾经一度非常流行,目前很多银行里还在使用以webservcie+XML的远程数据传输方式,但是由于XML传输的数据量比较大,会有很多冗余的字节,随着微服务架构的普及,越来越多的公司采用json的方式来进行序列化,json和xml一样,传输的数据格式有很好的可读性,但json的数据量一般比xml小好几倍,相对来说内存开销和传输速度都比xml有优势
目前常用的json序列化的框架有:fastjson,Gson,Jackson
fastjson: JSON.toJSONString(Object obj);
JSON.parseObject(String text, Class clazz);
Gson: gson.toJson(Object obj);
gson.fromJson(String text,Class clazz);
Jackson :objectMapper.readValue(String text,Class clazz);
objectMapper.writeValueAsString(Object obj);
不过当新增字段的时候,对于Jackson 需要 设置 objectMapper忽略没有的字段,否则在反序列化的时候会有异常
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Hessian是支持跨语言传输的二进制序列化协议,相对于java默认的序列化机制来说Hessian有更好的性能和易用性
Dubbo采用的就是Hessian序列化方式,不过Dubbo对其进行了重构
和ObjectOutputStream一样,Hessian要求被序列化的对象实现序列化接口Serializable接口。
/**
* @Description
* @Author chenpp
* @Date 2019/7/30 16:29
*/
public class HessianSerializer implements ISerializer {
// 对于使用Transient修饰的属性,会在序列化和反序列化的时候被忽略,但是不能使用writeObject()和readObject()来解决这个问题
public <T> byte[] serialize(T obj) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(bos);
try {
ho.writeObject(obj);
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(bos != null)
bos.close();
if(ho != null)
ho.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return new byte[0];
}
public <T> T deserialize(byte[] data, Class<T> clazz) {
ByteArrayInputStream bis = new ByteArrayInputStream(data);
HessianInput hi = new HessianInput(bis);
try {
return (T)hi.readObject();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
目前上述四种序列化方式中,同样的对象User(name ="chenpp",age = 21),进行序列化的结果如下:
| ObjectOutputStream | FastJson | XStream | Hessian | Protobuf |
使用transient是否会导致该属性不序列化 | √ | √ | √ | √ | X |
使用什么办法可以使transient失效 | 实体类重写writeObject readObject |
| 实体类重写writeObject readObject |
|
|
相同对象序列化后byte[]大小 | 100 | 26 | 247 | 58 | 10 |
排除Protobuf.序列化出来字节数最小的是json,序列化后为: { "name":"chenpp","age":21} 有26个字节
之后会再写一篇文章介绍下Google的Proto Buffer,分析下为什么Protobuf可以使用这么小的空间完成对象的序列化