java 序列化 serialVersionUID transient

原创 2015年11月19日 22:31:23

问题再现

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()=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  对象流与序列化

版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

java中序列化的serialVersionUID解释

serialVersionUID: 字面意思上是序列化的版本号,这个在刚刚接触java编程时,学序列化大家一般都不会注意到,在你一个类序列化后除非你强制去掉了myeclipse中warning的功能,...

深入理解Java序列化中的SerialVersionUid

一、前言 SerialVersionUid,简言之,其目的是序列化对象版本控制,有关各版本反序列化时是否兼容。如果在新版本中这个值修改了,新版本就不 兼容旧版本,反序列化时会抛出InvalidC...

【Java基础】序列化之serialVersionUID

为什么需要serialVersionUID?序列化很大部分的作用是持久化到本地中,那么有个问题就是如果在还原也就是反序列化这些本地数据的时候,原先的类结构已经发生了改变,存在本地的数据代表着的是旧的数...

java序列化和serialVersionUID

1、序列化: Java代码   序列化可以将一个java对象以二进制流的方式在网络中传输并且可以被持久化到数据库、文件系统中,反序列化则是可以把之前持久化在数据库或文件系统中的...
  • JIESA
  • JIESA
  • 2016年04月20日 11:26
  • 328

Java中序列化的serialVersionUID作用

Java序列化是将一个对象编码成一个字节流,反序列化将字节流编码转换成一个对象。 序列化是Java中实现持久化存储的一种方法;为数据传输提供了线路级对象表示法。 Java的序列化机制是通过在运行时判...

java中序列化的serialVersionUID解释

serialVersionUID: 字面意思上是序列化的版本号,这个在刚刚接触java编程时,学序列化大家一般都不会注意到,在你一个类序列化后除非你强制去掉了myeclipse中warning的功能,...
  • cselmu9
  • cselmu9
  • 2014年12月13日 11:58
  • 584

JAVA中,对象的序列化与serialVersionUID

只有可序列化的对象,才可以被文件存储或网络传输。 要想让对象可序列化,只需让它implements java.io.Serializable即可。...

Java中序列化的serialVersionUID作用

Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的seri...

Java序列化和serialVersionUID

简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:java 序列化 serialVersionUID transient
举报原因:
原因补充:

(最多只允许输入30个字)