Java序列化与反序列化 & 深拷贝

package com.main.domain;
public enum Gender {
	// 枚举类型都会默认继承类java.lang.Enum,而该类实现了Serializable接口,所以枚举类型对象都是默认可以被序列化的。
	MALE, FEMALE
}


package com.main.domain;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * Java序列化采用了一种特殊的序列化算法,其算法内容如下: 
 * 所有保存到磁盘中的对象都有一个序列化编号; 
 * 当程序视图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未(在本次虚拟机)中序列化过,系统才会将该对象转换成字节序列并输出; 
 * 如果某个对象已经序列化过,即使该对象某些属性有改动,程序也只是直接输出一个序列化编号,而不是再次重新序列化该对象。
 */
public class Person implements Externalizable {
	// Serializable接口中没有任何方法属性域,可以理解为一个标记,即表明这个类可以序列化。
	// Externalizable继承于Serializable,有两个方法,当使用该接口时,序列化的细节需要由程序员去完成。

	// 对象序列化不会关注类中的方法、static Field(即静态Field)、transient Field(也被称为瞬态Field)。
	// 序列版本UID,目的是序列化对象版本控制。如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException。
	private static final long serialVersionUID = 1L;

	private String name = null;
	transient private Integer age = null;// 当某个字段被声明为transient后,默认序列化机制就会忽略该字段。
	private Gender gender = null;

	// 使用Externalizable进行序列化,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。
	// 因此实现Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public,否则报错。
	public Person() {
		System.out.println("none-arg constructor of person");
	}

	public Person(String name, Integer age, Gender gender) {
		System.out.println("arg constructor of person");
		this.name = name;
		this.age = age;
		this.gender = gender;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Integer getAge() {
		return age;
	}

	public void setAge(Integer age) {
		this.age = age;
	}

	public Gender getGender() {
		return gender;
	}

	public void setGender(Gender gender) {
		this.gender = gender;
	}

	@Override
	public String toString() {
		return "Person [name=" + name + ", age=" + age + ", gender=" + gender
				+ "]";
	}

	// /////////////////////////////Serializable相关的自定义序列化机制:////////////////////////////////////////////////////

	// writeObject()与readObject()都是private方法,那么它们是如何被调用的呢?毫无疑问,是使用反射。
	// 详情可见ObjectOutputStream中的writeSerialData方法,以及ObjectInputStream中的readSerialData方法。

	private void writeObject(ObjectOutputStream out) throws IOException {
		System.out.println("writeObject()");
		out.defaultWriteObject();// defaultWriteObject()方法会执行默认的序列化机制,此时会忽略掉age字段。
		out.writeInt(age);// writeInt()方法显示地将age字段写入到ObjectOutputStream中,即使该字段为transient。
	}

	private void readObject(ObjectInputStream in) throws IOException,
			ClassNotFoundException {
		System.out.println("readObject()");
		in.defaultReadObject();
		age = in.readInt();
	}

	// 当反序列化是遇到如下异常会自动调用readObjectNoData方法:i. 序列化版本不兼容;ii. 输入流被篡改或者损坏;
	// 该方法的目的就是在发生上面这些异常时给对象提供一个合理的值(比如默认值或者全是0、null之类的,视具体情况而定)。
	// 但是如果反序列化时接受的类不存在还是会抛出异常的,毕竟该方法最终还是会还原出一个对象来,而对象的存在是以类的存在为前提的,所以没有类还是不行的。
	private void readObjectNoData() throws ObjectStreamException {
		System.out.println("readObjectNoData()");
	}

	/**
	 * 无论是实现Serializable接口还是Externalizable接口,程序在自定义序列化之前,都会先调用writeReplace()方法,
	 * 在序列化对象时将该对象替换成方法返回替换后的对象,方法返回的对象要与序列化前的对象要兼容,否则,会抛出 ClassCastException。
	 *
	 * 注意:
	 * a.实现writeReplace就不要实现writeObject了,因为writeReplace的返回值会被自动写入输出流中,就相当于自动这样调用:writeObject(writeReplace());
	 * b.因此writeReplace的返回值(对象)必须是可序列化的;
	 * c.但如果返回的是自定义类型的对象,那么该类型必须是彻底实现序列化的。
	 *
	 * writeReplace的替换如何在反序列化时被恢复?
	 * i.注意:不是用readResolve恢复,readResolve并不是用来恢复writeReplace的。
	 * ii.writeReplace替换后无法恢复,即对象被彻底替换了,也就是说使用ObjectInputStream读取的对象只能是被替换后的对象,只能在读取后自己手动恢复。
 	 *	
	 * 结论:writeObject只和readObject配合使用,一旦实现了writeReplace在写入时进行替换就不再需要writeObject和readObject。
	 * 因为替换就已经是彻底的自定义,比writeObject/readObject更彻底!
	 */
	private Object writeReplace() throws ObjectStreamException {
		System.out.println("writeReplace()");
		List list = new ArrayList();
		name += " James";
		list.add(name);
		list.add(age);
		list.add(gender);
		return list;
	}

	/**
	 * 无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。
	 * 实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象,而被创建的对象则会被垃圾回收掉。
	 *
	 * readResolve方法起到的作用:
	 * i.  调用该方法之前会先调用readObject反序列化得到对象;
	 * ii. 接着,如果该方法存在则会自动调用该方法;
	 * iii.在该方法中可以正常通过this访问到刚才反序列化得到的对象的内容;
	 * iv. 然后可以根据这些内容进行一定处理返回一个对象;
	 * v. 该对象将作为ObjectInputStream的readObject的返回值(即该对象将作为对象输入流的最终输入)。
	 *
	 * readResolve最重要的应用就是保护性恢复单例、枚举类型的对象。
	 */
	private Object readResolve() throws ObjectStreamException {
		System.out.println("readResolve()");
		return Integer.MAX_VALUE;
	}
	// /////////////////////////////////Serializable介绍结束/////////////////////////////////////////////////////////////////////////

	// ///////////////////////////////Externalizable接口中的两个方法/////////////////////////////////
	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		System.out.println("writeExternal()");
		out.writeObject(name);
		out.writeInt(age);
		out.writeObject(gender);
	}

	@Override
	public void readExternal(ObjectInput in) throws IOException,
			ClassNotFoundException {
		System.out.println("readExternal()");
		name = (String) in.readObject();
		age = in.readInt();
		gender = (Gender) in.readObject();

	}
	// ///////////////////////////////Externalizable介绍结束/////////////////////////////////
} 
  


package com.main.domain;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

//http://www.blogjava.net/jiangshachina/archive/2012/02/13/369898.html
//http://blog.csdn.net/p106786860/article/details/17953223
//http://blog.csdn.net/lirx_tech/article/details/51303966
public class SimpleSerial {
	public static void main(String[] args) throws Exception {
		File file = new File("person.out");
		ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(
				file));
 
		Person person = new Person("LeBron", 23, Gender.MALE);
		oout.writeObject(person);
		oout.close();

		System.out.println("===================序列化结束===================\n"
				+ "\n==================反序列化开始===================");
		// 反序列化读取的仅仅是Java对象的数据,而不是Java类,因此采用反序列化恢复Java对象时,必须提供Java对象所属的class文件,否则会引发ClassNotFoundException异常。

		ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
		Object newPerson = oin.readObject(); // 没有强制转换到Person类型
		oin.close();
		System.out.println(newPerson);
	}
}

深拷贝:

序列化主要是为了避免重写比较复杂对象的深复制的clone()方法,也可以程序实现断点续传等等功能。把对象写到流里的过程是串行化(Serilization)过程,但是在Java程序师圈子里又非常形象地称为“冷冻”或者“腌咸菜(picking)”过程;而把对象从流中读出来的并行化(Deserialization)过程则叫做 “解冻”或者“回鲜(depicking)”过程。

应当指出的是,写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面,因此“腌成咸菜”的只是对象的一个拷贝,Java咸菜还可以回鲜。

在Java语言里深复制一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里(腌成咸菜),再从流里读出来(把咸菜回鲜),便可以重建对象。序列化的前提是对象以及对象内部所有引用到的对象都是可串行化的,否则,就需要仔细考察那些不可串行化的对象或属性可否设成transient,从而将之排除在复制过程之外。

public Object deepClone() {    
   //将对象写到流里    
   ByteArrayOutoutStream bo=new ByteArrayOutputStream();    
   ObjectOutputStream oo=new ObjectOutputStream(bo);    
   oo.writeObject(this);    
   //从流里读出来    
   ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray());    
   ObjectInputStream oi=new ObjectInputStream(bi);    
   return(oi.readObject());    
}

关于serialVersionUID:

serialVersionUID: 字面意思上是序列化的版本号,凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量private static final long serialVersionUID
(1)serialVersionUID有两种生成方式:

   I. 采用默认的,这种方式生成的serialVersionUID是1L,例如:private static final long serialVersionUID = 1L;
   II. 根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,例如:private static final long serialVersionUID = 4603642343377807741L;

(2)那么serialVersionUID(序列化版本号)到底有什么用呢?

serialVersionUID存在的目的是序列化对象版本控制,有关各版本反序列化时是否兼容。
如果在新版本中serialVersionUID值修改了,新版本就不兼容旧版本,反序列化时会抛出InvalidClassException异常。
如果修改较小,比如仅仅是增加了一个属性,希望向下兼容,老版本的数据都能保留,那就不用修改;
如果修改较大,比如删除了一个属性或者更改了类的继承关系,必然不兼容旧数据,这时就应该手动更新版本号。

类的serialVersionUID的默认值完全依赖于Java编译器的实现,是Java运行时环境根据类的内部细节自动生成的,对于同一个类,用不同的Java编译器编译,有可能会导致不同的serialVersionUID,也有可能相同。

为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示的定义serialVersionUID,并为它赋予明确的值

你可能感兴趣的:(Java)