问题再现
User类实现了序列化,但是没有声明版本号,这个对象放在memcache中,User新添加了1个字段后,把之前的对象从缓存中取出来时,出现了InvalidClassException,为什么会出现这个错误?
序列化
序列化就是将对象转为流,用于传输或保存。
序列化的是“对象状态”,所以就不包括静态变量;
反序列化是从流中读取对象;
序列化会递归序列化属性的引用。如果父类实现了序列化,那么子类也实现了序列化。这一条跟父类实现接口,子类也实现接口,一个道理。
序列化的应用场景
序列化通常发生在由对象需要保存或者传输的情况,比如以下三种情况:
1、对象保存到硬盘上。
2、远程调用对象(RMI)
3、分布式系统中,比如memcached缓存系统中,存储的值需要由客户端传输到服务端,所以key和value都必须序列化。
4、tomcat session持久化
实现序列化的方法
实现序列化有两种方式,实现serializable或者externalizable,这两个都是接口,并且externalizable继承serializable
下边分别进行说明
1、实现Serializable接口,
serializable接口是个标记接口,不需要实现方法。
public class Person implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
public Person(String firstname, String lastname) {
super();
this.firstname = firstname;
this.lastname = lastname;
}
private String firstname;
private String lastname;
//getter and setter method
@Override
public String toString() {
return "Person [firstname=" + firstname + ", lastname=" + lastname
+ "]";
}
public static void main(String[] args) {
File f = new File("out.file");
Person obj = new Person("michael","jack");
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(obj);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
if(oos!=null){
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**/
Person p;
try {
p = (Person) new ObjectInputStream(new FileInputStream(f)).readObject();
System.out.println(p);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Person [firstname=michael, lastname=jack]
如果某个字段,比如密码,敏感性比较高,不想被保存到文件中,可以使用transient修饰。
这里我们添加pwd字段,并用transient修饰。
(transient [ˈtrænziənt] adj 瞬时的,临时的,转瞬即逝的)
public class Person implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
public Person(String firstname, String lastname) {
super();
this.firstname = firstname;
this.lastname = lastname;
}
private String firstname;
private String lastname;
private transient String pwd;
//getter and setter method
@Override
public String toString() {
return "Person [firstname=" + firstname + ", lastname=" + lastname
+ ", pwd=" + pwd + "]";
}
public static void main(String[] args) {
File f = new File("out.file");
/* */
Person obj = new Person("michael", "jack");
obj.setPwd("mypwd");
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(obj);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**/
Person p;
try {
p = (Person) new ObjectInputStream(new FileInputStream(f))
.readObject();
System.out.println(p);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Person [firstname=michael, lastname=jack, pwd=null]
我们可以看到pwd值是null
2、实现Serializable接口,使用writeObject和readObject,定制序列化。
如果想特殊定制序列化,根据API我们可以使用writeObject和readObject覆盖掉默认的序列化
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeObject(pwd);
}
private void readObject(java.io.ObjectInputStream in) throws IOException,
ClassNotFoundException {
in.defaultReadObject();
pwd = (String) in.readObject();
}
Person [firstname=michael, lastname=jack, pwd=mypwd]
我们看到,pwd加了transient,按照默认序列化机制,反序列化出来的对象应该是没有值的,但是在反序列化时,仍然有值。这证明,我们的定制序列化成功了。
另外,反序列化Person类没有调用无参构造并且writeobject、readobject都是private的,说明这种反序列化使用的反射。
2、实现Externalizable,并覆盖writeexternal和readexternal方法
public class Person implements Externalizable {//
/**
*
*/
private static final long serialVersionUID =1L;
public Person() {
super();
System.out.println("Person执行无参构造方法");
}
public Person(String firstname, String lastname) {
super();
this.firstname = firstname;
this.lastname = lastname;
}
private String firstname;
private String lastname;
private transient String pwd;
//getter and setter method
@Override
public String toString() {
return "Person [firstname=" + firstname + ", lastname=" + lastname
+ ", pwd=" + pwd + "]";
}
/*
* externalized接口,可外部化的
* @see java.io.Externalizable#writeExternal(java.io.ObjectOutput)
*/
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("writeExternal....");
out.writeObject(firstname);
out.writeObject(lastname);
out.writeObject(pwd);
}
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
System.out.println("readExternal....");
firstname = (String) in.readObject();
lastname = (String) in.readObject();
pwd = (String) in.readObject();
}
public static void main(String[] args) {
File f = new File("out.file");
/**/
Person obj = new Person("firstname","lastname");
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(obj);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**/
Person p = null;
try {
p = (Person) new ObjectInputStream(new FileInputStream(f))
.readObject();
System.out.println(p);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(obj == p);
}
}
写到这里你肯定在想serializable和externalizable有什么区别,两者都可以实现序列化,为什么还需要分开两个接口?
serializable是标记接口,只实现接口,就可实现序列化,并且可以通过writeobject和readobject进行定制,这两个方法中有默认序列化方法,仅仅需要对个别字段进行特殊处理,反序列化使用反射创建对象。
实现externalizable,必须实现writeexternal和readexternal方法,在方法中要对对象的所有字段进行维护,反序列化使用无参构造方法创建对象。
3、单例情况的特殊处理
如果序列化的对象是单例,那么类构造方法是私有的,这种情况,使用readresolve,readresolve会在readobject之后执行,改变返回的值。
public class Person implements Serializable {//
/**
*
*/
private static final long serialVersionUID =1L;
//
private static class InstanceHolder{
private static final Person p = new Person("privatefirstname","privatelastname");
}
public static Person getinstance(){
return InstanceHolder.p;
}
private Person() {
super();
System.out.println("Person执行无参构造方法");
}
private Person(String firstname, String lastname) {
super();
this.firstname = firstname;
this.lastname = lastname;
}
private String firstname;
private String lastname;
// private String middlename;
private transient String pwd;
//getter and setter method
@Override
public String toString() {
return "Person [firstname=" + firstname + ", lastname=" + lastname
+ ", pwd=" + pwd + "]";
}
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
System.out.println("writeObject.....");
out.defaultWriteObject();
}
private void readObject(java.io.ObjectInputStream in) throws IOException,
ClassNotFoundException {
System.out.println("readObject.....");
in.defaultReadObject();
}
Object readResolve() throws ObjectStreamException{
System.out.println("readresolve.....");
return Person.getinstance();
}
public static void main(String[] args) {
File f = new File("out.file");
/**/
// Person obj = new Person("firstname","lastname");
Person obj = Person.getinstance();
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(obj);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**/
Person p = null;
try {
p = (Person) new ObjectInputStream(new FileInputStream(f))
.readObject();
System.out.println(p);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(obj == p);
}
}
writeObject.....
readObject.....
readresolve.....
Person [firstname=privatefirstname, lastname=privatelastname, pwd=null]
true
反序列化原则
java(jvm)会尽可能的将流转换为对象,jvm首先会比对两个类中存储的版本号,如果版本号不相等,那么会抛出InvalidClassException。然后再比较成员字段信息,jvm中类的字段比流中的字段多或者少,只要类型不发生变化,就会转换成功;也就是说,如果jvm中的类信息相比较文件中存储的类信息多出字段,那么多出的字段默认值是null,如果jvm中的类信息相比较文件中存储的类信息少了字段,那么就舍弃。如果字段类型发生了变化,会反序列化失败;
序列化版本号
版本号用来标志类的版本,包括默认版本号和显式版本号;
默认版本号:如果类实现了Serializable接口,但是没有声明serialVersionUID,编译器会根据类成员字段、父类、接口等计算出一个默认版本号,存储在类中。由于默认版本号高度依赖编译器,这种方式兼容性不可靠。所以,强烈建议显式声明版本号。
显式版本号:显式声明一个版本号,比如private static final long serialVersionUID=1L,它不会随着编译器或者字段信息而改变,保证了平台间的兼容性。
如果类升级了,我们不想再与以前旧版本兼容,那么可以改变版本号,旧版本类在反序列化时就会抛出InvalidClassException。
在eclipse中,如果类实现了序列化接口,但是没有声明版本号,它会提示两种解决方案,default serial version id值是1,generated serial version id值是根据成员字段、父类、接口等计算出来,使用任何一种都可以,只要我们理解版本号的作用。
补充
1、父类未实现序列化,子类实现序列化,父类的状态不会被保存。要想父类的状态被保存,父类也必须实现序列化。
2、每个对象都有序列号,类似版本号,对一个对象的多次保存,仅会保存第一次的对象。对象的版本号,可以当做内存地址。
Person obj = new Person("firstname0","lastname0");
oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(obj);
// oos.reset();
obj.setFirstname("firstnam1");
obj.setLastname("lastname1");
// obj = new Person("firstname1","lastname1");
oos.writeObject(obj);
// oos.reset();
obj.setFirstname("firstnam2");
obj.setLastname("lastname2");
// obj = new Person("firstname2","lastname2");
oos.writeObject(obj);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
p = (Person)ois.readObject();
Person p1 = (Person)ois.readObject();
Person p2 = (Person)ois.readObject();
System.out.println("序列化后:"+p);
System.out.println("序列化后:"+p1);
System.out.println("序列化后:"+p2);
序列化后:Person [firstname=firstname0, lastname=lastname0, toString()[email protected]]
序列化后:Person [firstname=firstname0, lastname=lastname0, toString()[email protected]]
序列化后:Person [firstname=firstname0, lastname=lastname0, toString()[email protected]]
保存的是第一次,因为这三个obj的内存地址是一样,在序列化时,首先会进行判断,发现已经存在,那么仅仅保存上一个的引用符号。据此推理,如果我们每次都是new对象,那么就不会覆盖,比如
Person obj = new Person("firstname0","lastname0");
oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(obj);
// oos.reset();
// obj.setFirstname("firstnam1");
// obj.setLastname("lastname1");
obj = new Person("firstname1","lastname1");
oos.writeObject(obj);
// oos.reset();
// obj.setFirstname("firstnam2");
// obj.setLastname("lastname2");
obj = new Person("firstname2","lastname2");
oos.writeObject(obj);
oos.close();
序列化后:Person [firstname=firstname0, lastname=lastname0, toString()[email protected]]
序列化后:Person [firstname=firstname1, lastname=lastname1, toString()[email protected]]
序列化后:Person [firstname=firstname2, lastname=lastname2, toString()[email protected]]
[email protected]有对象的状态。每次写入之前,调用reset也可以达到不会被覆盖的效果。
Person obj = new Person("firstname0","lastname0");
oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(obj);
oos.reset();
obj.setFirstname("firstnam1");
obj.setLastname("lastname1");
// obj = new Person("firstname1","lastname1");
oos.writeObject(obj);
oos.reset();
obj.setFirstname("firstnam2");
obj.setLastname("lastname2");
// obj = new Person("firstname2","lastname2");
oos.writeObject(obj);
总结
1、深入理解版本号的作用,才能决定何时改变版本号,版本号可能会导致反序列化失败。
2、深入理解序列化的作用,然后就能知道序列化的应用场景,联系实际项目理解序列化。
3、实现serializable接口,反序列化时,不使用无参构造,使用反射恢复对象。
4、实现externalizable接口,反序列化时,使用无参构造恢复对象。
5、实现serializable,readresolve实现单例
参考
1、understand the serialversionuid
3、java核心卷2 对象流与序列化
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/20282.html