(一)什么是序列化和反序列化
序列化和反序列化是将对象转化成字节数组以方便保存或者用于网络传输,这个对象可以是一个图片,一个字符串,一个class等等,常见的序列化格式有字节数组,json格式,xml格式,更加高效的有google开源的Protocol Buffers,以及Apache Avro。
(二)为什么需要序列化和反序列化
(1)实现数据持久化,一般jvm的里面数据,在java程序退出时,所有的状态都不会保留,通过序列化可以将需要的数据给持久化到磁盘文件或者数据库,这样就可以在下次jvm启动的时候再把数据重新还原出来。
(2)利用序列化实现远程通信,即在网络上传送对象的字节序列,这种场景一般在socket或者rpc的服务中比较常见。
(三)Java里面如何实现序列化和反序列化
在java里面有两种方式可以实现对象的序列化:
(1)实现Serializable接口的类,jdk会自动帮我们序列化该类所有的信息, 但如果用户定义了writeObject和readObject方法,那么在序列化和反序列化的时候会通过反射优先调用自定义的方法
(2)实现Externalizable接口的类,需要用户自定义序列化和反序列化的逻辑,分别重写writeExternal和readExternal方法。
下面看一个例子,首先我们定义一个Person类并实现了序列化接口:
package cn.xby;
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private transient String address;
private String name;
private int age;
@Override
public String toString() {
return "Person [address=" + address + ", name=" + name + ", age=" + age + "]";
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
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;
}
public Person(String address, String name, int age) {
super();
this.address = address;
this.name = name;
this.age = age;
}
public Person() {
super();
}
}
然后我们定义了帮助实现序列化和反序列化的工具类:
package cn.xby;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class SerializeTools {
/**
* 将任何实现了序列化接口的对象转成字节数组
* @param obj
* @return
* @throws Exception
*/
public static byte[] toBytes(Serializable obj) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
return byteArrayOutputStream.toByteArray();
}
/**
* 将任何序列化的字节数组给还原成对象
* @param bytes
* @return
* @throws Exception
*/
public static Object toObj(byte[] bytes) throws Exception {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object obj = objectInputStream.readObject();
return obj;
}
/**
* 将一个实现了序列化的对象给序列化成文件
* @param obj
* @param storePath
* @throws Exception
*/
public static void toFile (Serializable obj,String storePath) throws Exception {
FileOutputStream fileOutputStream = new FileOutputStream(new File(storePath));
@SuppressWarnings("resource")
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(obj);
}
/**
* 将序列化对象还原成文件
* @param storePath
* @return
* @throws Exception
*/
public static Object fromFile(String storePath) throws Exception {
FileInputStream fileInputStream = new FileInputStream(new File(storePath));
@SuppressWarnings("resource")
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Object obj = objectInputStream.readObject();
return obj;
}
}
然后看下我们的测试类,分别测试文件的序列化和字节的序列化
package cn.xby;
public class TestSerialize {
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
Person person = new Person("北京海淀","张三",25);
fileTest(person);//文件测试序列化和反序列化
System.out.println("======================================");
byteTest(person);//字节测试序列化和反序列化
}
public static void fileTest(Person p1) throws Exception {
String storePath = "D://temp.out";
System.out.println("基于文件序列化前:" + p1);
SerializeTools.toFile(p1, storePath);
Person p2 = (Person)SerializeTools.fromFile(storePath);
System.out.println("基于文件序列化后:" + p2);
}
public static void byteTest(Person p1) throws Exception {
System.out.println("基于字节序列化前:" + p1);
byte[] bytes = SerializeTools.toBytes(p1);//序列化成字节数组
Person p2 = (Person)SerializeTools.toObj(bytes);//反序列化成对象
System.out.println("基于字节反序列化后:" + p2);
}
}
运行后输出如下:
基于文件序列化前:Person [address=北京海淀, name=张三, age=25]
基于文件序列化后:Person [address=null, name=张三, age=25]
======================================
基于字节序列化前:Person [address=北京海淀, name=张三, age=25]
基于字节反序列化后:Person [address=null, name=张三, age=25]
细心的同学可能已经发现地址这个字段,在反序列化后字段值丢失了,这里说明下:
(1)在java里面transient关键词修饰的成员变量是不会被序列化的,这一点在上面的输出中已经得到验证了,注意transient关键词只能修饰成员变量,不能修饰类和方法
(2)在java里面static关键词修饰的字段也是不会被序列化的,因为这个是类的字段,而序列化是针对对象的。
引申一下:java的内存分配有栈和堆以及永久代,栈中存放基本变量,数组和对象引用,堆中存放对象,当有static修饰的变量或方法会被放到永久代里面。它先于对象而存在,不依赖实例,无论是变量,方法,还是代码块,只要用static修饰,就是在类被加载时就已经准备好了,也就是可以被使用或者已经被执行,都可以脱离对象而执行,所以在类加载时静态变量的值其实已经还原出来了之后才是反序列化出来成员变量的值。
(3)在上面的Person类里面,相信大家还看到了一个用static final long修饰的 serialVersionUID字段,这个字段的功能是用来标识类版本的兼容性:
举个例子,假如现在没有定义serialVersionUID这个字段,jdk默认是根据类信息计算一个版本值,在类已经被序列化成文件后,我们又修改了类结构,比如新增了几个字段,这个时候拿着新版本的类去反序列化旧版本的类,就会抛出下面的异常:
意思就是版本不一致,导致失败,如果我们定义这个值,并且新旧版本的值一样,不管新增没新增字段,都可以反序列化成功,默认新增字段的值是jdk给成员变量初始化的值,比如字符串就是null。
(四)定制自己的序列化和反序列化方法
上面提到过实现了Serializable接口的类,我们可以重写下面的方法来自定义序列化逻辑:
private void writeObject(ObjectOutputStream out) throws Exception {
System.out.println("call write");
out.writeObject(address);
out.writeObject(name);
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws Exception {
System.out.println("call read");
address = (String)in.readObject();
name = (String)in.readObject();
age = in.readInt();
}
再次执行测试方法,输出结果如下:
基于文件序列化前:Person [address=北京海淀, name=张三, age=25]
call write
call read
基于文件序列化后:Person [address=北京海淀, name=张三, age=25]
======================================
基于字节序列化前:Person [address=北京海淀, name=张三, age=25]
call write
call read
基于字节反序列化后:Person [address=北京海淀, name=张三, age=25]
这次我们发现了被transient修饰的address字段竟然也有值了,为什么?因为我们自定义序列化的时候把地址也给序列化了,所以这个时候无论你用不用transient关键词都无关紧要了。
注意如果实现了上面的方法其实和使用Externalizable就相差无几了,所以在这里不再给出Externalizable的例子
(五)什么时候应该readObject和writeObject
在effective java里面提到过:
当一个对象的物理表示方法与它的逻辑数据内容有实质性差别时,使用默认序列化形式有N种缺陷。
其实是建议我们重写的,这样可以更好的控制序列化的过程,如果能减少一些不必要的序列化的字段,其实对我们的程序性能也是一种提升。
总结:
本文介绍了Java里面序列化和反序列化功能和使用以及一些注意事项,序列化其实还提供了深度克隆的功能,尤其是当类里面的引用层次比较多及引用特别复杂的时候,通过序列化来深度拷贝对象也是一种比较便利的方法,除此之外,我们还应该知道序列化和反序列化和反射一样,弱化了java安全权限修饰符的作用,无论你privte还是protected修饰的字段,在序列化和反射面前都是无作用的,所以一些敏感信息的序列化尤其是在网络上传输的如密码,金钱什么的,都应该考虑加密或者其他安全措施。