1.什么是java序列化
Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,
即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,
并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。
使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。
必须注意地是,对象序列化保存的是对象的"状态",即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。
除了在持久化对象时会用到对象序列化之外,当使用RMI(远程方法调用),或在网络中传递对象时,都会用到对象序列化。
2.实现一个简单的java序列化程序
1.建立一个枚举类型的Gender.java
public enum Gender {
MALE,FEMALE
}
2.建立一个Person.java
import java.io.Serializable;
/**
* Created by hao.su on 14-5-11.
*/
public class Person implements Serializable {
private String name;
private transient Integer age;
private Gender gender;
public Person() {
System.out.println("non-arg constructor");
}
public Person(String name, Integer age, Gender gender) {
System.out.println("args constructor");
this.name = name;
this.age = age;
this.gender = gender;
}
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
public void setGender(Gender gender) {
this.gender = gender;
}
public Integer getAge() {
return age;
}
public Gender getGender() {
return gender;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", gender=" + gender +
'}';
}
}
3.建立一个简单的序列化测试类SimpleSerial.java
public class SimpleSerial {
public static void main(String[] args) {
File file = new File("person.out");
//写到文件
ObjectOutputStream objectOutputStream = null;
try {
objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
} catch (IOException e) {
e.printStackTrace();
}
Person person = new Person("suhao", 23, Gender.MALE);
try {
objectOutputStream.writeObject(person);
objectOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
//读取文件
ObjectInputStream objectInputStream = null;
try {
objectInputStream = new ObjectInputStream(new FileInputStream(file));
} catch (IOException e) {
e.printStackTrace();
}
Object newPerson = null;
try {
newPerson = objectInputStream.readObject();
objectInputStream.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(newPerson);
}
}
程序运行效果:
args constructor
Person{name='suhao', age=null, gender=MALE}
结论分析: 在java中只要一个类实现了java.io.Serializable接口那么该类就是可以被是实力化的,上面的Gender.java 是
一个枚举类型,我们知道枚举类型是实现了Serializable接口的,那么该类是可以被实例化的
在person类中实现了序列化接口,其中有三个字段
注意到,在反序列化重新读取对象的时候并没有调用构造方法,就像将person对象直接还原一样,我们也可以在其他地方将person还原,但是必须要保证,在classpath中有person.class,否则要抛出ClassNotFoundException
3.Serializable的作用
我们可以看看ObjectOutputStream的源码就知道,作用了,
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());
}
}
这下知道为什么只有实现了Serializable接口才能被序列化了吧
4.默认的序列化机制
在默认的序列化机制中,不仅会序列化当前本省的对象,而且对象所引用的对象也要被序列化,这些对象引用的其他对象也要被序列化,如此继续。
5.控制序列化的行为
5.1transient 关键字
在person类中的age前面加上transient关键字
private transient Integer age;
此时再运行程序:
args constructor
Person{name='suhao', age=null, gender=MALE}
发现age的值为空;当一个字段被声明为transient关键字之后,默认的序列化机制,就会忽略该字段
5.2 writeObject 和readObject
在person类中添加者两个方法
private void writeObject(ObjectOutputStream out) throws IOException{
out.defaultWriteObject();
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}
结果为:
args constructor
Person{name='suhao', age=23, gender=MALE}
age的值写入了
在writeObject 中先调用了默认的defaultObject 方法,此时age字段将会被忽略,在调用writeInt方法将age字段写入,读取时也是这样。
5.3Externalizable接口
JDK中提供了另一个序列化接口--Externalizable,使用该接口之后,之前基于Serializable接口的序列化机制就将失效
将person类修改如下
package org.qunar.training.serializationdemo;
import java.io.*;
/**
* Created by hao.su on 14-5-11.
*/
public class Person implements Externalizable {
private String name;
transient private Integer age = null;
private Gender gender;
public Person() {
System.out.println("non-arg constructor");
}
public Person(String name, Integer age, Gender gender) {
System.out.println("args constructor");
this.name = name;
this.age = age;
this.gender = gender;
}
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
public void setGender(Gender gender) {
this.gender = gender;
}
public Integer getAge() {
return age;
}
public Gender getGender() {
return gender;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", gender=" + gender +
'}';
}
private void writeObject(ObjectOutputStream out) throws IOException{
out.defaultWriteObject();
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
}
在执行之:
args constructor
non-arg constructor
Person{name='null', age=null, gender=null}
从结果中可以看到,调用了两次构造器,并且字段都没有序列化
Externalizable继承于Serializable,当使用该接口时,序列化的细节需要由程序员去完成。如上所示的代码,由于writeExternal()与readExternal()方法未作任何处理,那么该序列化行为将不会保存/读取任何一个字段。这也就是为什么输出结果中所有字段的值均为空。
另外,若使用Externalizable进行序列化,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。这就是为什么在此次序列化过程中Person类的无参构造器会被调用。由于这个原因,实现Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public
另外,若使用Externalizable进行序列化,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。这就是为什么在此次序列化过程中Person类的无参构造器会被调用。由于这个原因,实现Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public
修改实现Person类中的两个方法:
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
out.writeObject(gender);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
gender = (Gender) in.readObject();
}
5.4readResolve方法
序列化不能保证单列模式的特性,要想保证单列模式的特性,就用readResolve来解决
private Object readResolve() throws ObjectStreamException {
return InstanceHolder.instatnce;
}
6.序列化中各种问题
1.序列化ID问题
虚拟时候允许反序列化,不仅取决于类路径是否一致,一个非常中重要的一点事两个类的序列化ID是否一致
2.静态变量序列化问题
序列化不保存静态变量,这其实比较容易理解,序列化保持的是对象的状态,而静态变量是属于类的状态
3.父类序列化
一个子类实现serializable接口,他的父类没有实现,序列化该子类的时候,然后反序输出父类定义的字段,该字段的值于序列化的时候不同
要想让父类进行序列化,也让父类进行序列化
4.敏感字段的加密
在序列化过程中,jvm会调用对象的writeObject和readObject方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,那么默认调用ObjectoutputStream的defaultWriteObject方法以及defaultReadObject方法
5.序列化存储规则
将同一个对象两次写入文件,打印存储第一次存储大小和 写第二次后的大小,然后从文件中反序列化出两个对象,比较这两个对象是否为同一个对象,结果是第二次邪热文件只是增加了5个字节,并且两个对象时相等。
这是为什么呢?
java序列化机制为了节省磁盘空间,当些人文件的为同一个对象时,并不会为对象的内容进行存储,而只是在存储一份引用,上面郑家的5个字节的存储问题,就是新增应用和一些控制信息的空间,反序列化时候恢复引用
将一个对象两次保存到文件中,在第一次保存之后,修改对象的一个字段,在反序列化进行输出时候发现都是修改前的值。这是为什么呢?因为在第二次保存的时候虚拟机发现已经有这样一个对象了,所以第二次保存的只是一个引用,所以读取时都是第一次的对象。