java 序列化 serialVersionUID transient详解编程语言

问题再现

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值是根据成员字段、父类、接口等计算出来,使用任何一种都可以,只要我们理解版本号的作用。

java 序列化 serialVersionUID transient详解编程语言

补充

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

2、java serializable接口文档;

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

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/20282.html

(0)
上一篇 2021年7月19日
下一篇 2021年7月19日

相关推荐

发表回复

登录后才能评论