什么是序列化与反序列化?
序列化是将Java对象转换成与平台无关的二进制流,而反序列化则是将二进制流恢复成原来的Java对象,二进制流便于保存到磁盘上或者在网络上传输。
如何实现序列化与反序列化?
如果想要序列化某个类,就需要让该类实现Serializable接口或者Externalizable接口。
如果实现Serializable接口,由于该接口只是个"标记接口",接口中不含任何方法,序列化是使用ObjectOutputStream(处理流)中的writeObject(obj)方法将java对象输出到输出流中的,反序列化是使用ObjectInputStream中的readObject(in)方法将输入流中的Java对象还原出来。
实践
1.通过实现Serializable接口进行序列化与反序列化
public class Person implements Serializable {
private String name;
private int age;
//此处没有提供无参的构造函数
public Person(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;
}
}
public class SerializableTest {
public static void main(String[] args) {
//序列化
try{
FileOutputStream fileOutputStream = new FileOutputStream(new File("").getAbsolutePath()+"/object.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
Person person = new Person("小明",21);
objectOutputStream.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
}
try{
FileInputStream fileInputStream = new FileInputStream(new File("").getAbsolutePath()+"/object.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Person person = (Person) objectInputStream.readObject();
System.out.println("name:"+person.getName()+",age:"+person.getAge());
}catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
需要注意的是反序列读取的仅仅是java对象中的数据,而不是包含java类的信息,所以在反序列化时还需要对象所属类的字节码(class)文件(经测试序列化操作也需要),否则会出现ClassNotFoundException异常。
2.通过实现Externalizable接口进行序列化与反序列化
Externalizable接口继承自Serializable接口,在Java Bean类中实现接口中的writeExternal(out)和readExternal(in)方法,需要注意的是必须提供默认的无参构造函数,否则反序列化失败。
上面的Person类代码改为:
public class Person implements Externalizable {
private String name;
private int age;
//此处需要提供默认的无参构造函数
public Person() {
}
public Person(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;
}
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = in.readObject().toString();
this.age = in.readInt();
}
}
调用方法的时候传入out和in就可以了。
这两种序列化反序列化方式,前一种是使用默认的java实现,而后一种是自定义实现,可以在序列化中选择如何序列化,比如对某个属性加密处理。
注意序列化属性的顺序要和属性反序列化中的顺序一样,否则在反序列化时不能恢复出原来的对象。
3.实现Serializable接口实现自定义序列化
只需在JavaBean类中提供下面三个方法。
private void writeObject(java.io.ObjectOutStream out) throws IOException;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
其中writeObject和readObject方法和上面的类似。readObjectNoData方法是在序列化流不完整、序列化和反序列化版本不一致导致不能正确反序列时调用的容错方法。
使用默认的序列化方式,会将对象中的每个实例属性依次进行序列化,如果某个属性是一个类类型,那么需要保证这个类也是可以序列化的类,否则将不能序列化该对象。在Java的序列化机制中,被序列化后的对象都有一个编号,多次序列化同一个对象,除了第一次真正序列化对象外,其它都是保存一个序列化编号。这样的机制带来的问题是如果在序列化一个对象后,修改了对象中的属性,也不会生效。并不是对象中每个属性都需要序列化的,如被static修饰的属性是属于类的,而不是只属于某个对象。使用默认序列化方式,是不会将这些属性序列化的,在自定义的序列化方式中,我们也可以将这些属性忽略掉。除此之外,可以使用transient关键字来修饰某个属性,这样默认的序列化方式就不会序列化该属性了,自定义还是可以的。如果在反序列化时强行得到这些没有被序列化的值,得到的会是默认值(0或null)。
序列化和反序列化的版本问题
在java的序列化机制中,允许给类提供一个private static final 修饰的serialVersionUID类常量,来作为类版本的代号。这样即使类被修改了(如修改了方法),也会把修改前的类和修改后的类当成同一版本的类,序列化和反序列化照样可以正常使用。如果我们不显式的定义这个serialVersionUID,java虚拟机会根据类的信息帮我们自动生成,修改前和修改后的计算结果往往不同,造成版本不兼容而发生反序列化失败,另外由于平台的差异性,在程序移植中也可能出现无法反序列化。强大的IDE工具,也都有自动生成serialVersionUID的方法。
总结
- 序列化和反序列化方式可以分为三种,一种是实现Serializable接口使用默认的序列化和反序列化方式,一种是实现Serializable接口但是自定义序列化和反序列化方式,另外一种是实现Externalizable接口,实现接口中的方法。
- 序列化和反序列化要注意版本问题,自定义序列和反序列化时还要注意属性的顺序要保持一致,这些都可能会导致反序列化失败。