java 序列化 serialVersionUID transient

来源:互联网 发布:mac os 11 编辑:程序博客网 时间:2024/05/19 17:51

问题再现

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@Overridepublic 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@Overridepublic 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@Overridepublic 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@Overridepublic 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()=webmvct.cmd.Person@3f29a75a]序列化后:Person [firstname=firstname0, lastname=lastname0, toString()=webmvct.cmd.Person@3f29a75a]序列化后:Person [firstname=firstname0, lastname=lastname0, toString()=webmvct.cmd.Person@3f29a75a]


保存的是第一次,因为这三个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()=webmvct.cmd.Person@49b3d1e5]序列化后:Person [firstname=firstname1, lastname=lastname1, toString()=webmvct.cmd.Person@3c993730]序列化后:Person [firstname=firstname2, lastname=lastname2, toString()=webmvct.cmd.Person@6ef64f64]


objectoutputstream提供的reset方法用来清空写入流中所有对象的状态。每次写入之前,调用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

2、java serializable接口文档;

3、java核心卷2  对象流与序列化

0 0
原创粉丝点击