对象序列化与反序列化(Serializable、Externalizable )

对象序列化?

对象序列化的概念加入到语言中为了提供对两种主要特性的支持:

1 、远程方法调用

2 Java Beans 状态的保存与恢复

 

 

ObjectInput 接口继承 DataInput 接口

ObjectOutput 接口继承 DataOutput 接口

ObjectOutputStream 类实现了 DataOutput,ObjectOutput

ObjectInputStream 类实现了 DataInput,ObjectInput

 

 

ObjectOutputStream.defaultWriteObject() :将当前类的非静态和非瞬态字段写入此流。

ObjectInputStream.defaultReadObject() :从此流读取当前类的非静态和非瞬态字段。

 

反序列化时,该序列化对象所对应的类一定要在反序列化运行环境的classpath中找得到,不然在读 (readObject()) 序列化文件时因找不到相应的Class会抛出ClassNotFoundException异常。

 

一个类实现了序列化接口,则该类里的所有对象都要求实现序列化接口,不然在进行序列化进会抛异常。因为序列化好比深层克隆,它会序列化各个对象属性里的对象属性。 如果一个属性没有实现可序列化,而我们又没有将其用transient 标识, 则在对象序列化的时候, 会抛出java.io.NotSerializableException 异常。

使用 Serializable序列化

为了序列化一个对象,首先要创建某些 OutputStream 对象,然后将其封装在一个 ObjectOutput  Stream 对象内。这时,只需调用 writeObject() 即可将对象序列化,并将其发送给 OutputStream 要将一个序列重组为一个对象,需要将一个 InputStream 封装在 ObjectInputStream 内,然后调用 readObject() 。和往常一样,我们最后获得的是指向一个向上转型为 Object 的句柄,所以必须向下转型,以便能够直接对其进行设置。

 

以下示例演示了怎样通过实现Serializable标记接口来实现序列化:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Random;

//可序列化对象,实现了Serializable标志接口
class Data implements Serializable {
	private int n;

	public Data(int n) {
		this.n = n;
	}//默认构造器

	public String toString() {
		return Integer.toString(n);
	}
}

//可序列化对象,实现了Serializable标志接口,并形成一个网络对象
public class Worm implements Serializable {
	private static Random rand = new Random();
	//Worm对象里有Data数组,数组里又存放了Data对象
	private Data[] d = { new Data(rand.nextInt(10)), new Data(rand.nextInt(10)),
			new Data(rand.nextInt(10)) };
	private Worm next;//指向下一个Worm对象,如果是最后一个,则指向null
	private char c;

	public Worm(int i, char x) {//i表示构造几个这样的蠕虫对象,也即Worm对象的编号
		System.out.println("Worm constructor: " + i);
		c = x;
		if (--i > 0) {
			//指向的每个Worm对象的c为起始的x增加一,比如传进来
			//的x起始为 A,则下一个Worm对象的x就为B,依次类推
			next = new Worm(i, (char) (x + 1));
		}
	}

	//默认构造函数
	public Worm() {
		System.out.println("Default constructor");
	}

	//递归打印蠕虫对象信息
	public String toString() {
		String s = ":" + c + "(";
		for (int i = 0; i < d.length; i++) {
			s += d[i];
			if (i != d.length - 1) {
				s += " ";
			}
		}
		s += ")";
		//如果不是最后一个
		if (next != null) {
			s += next.toString();
		}
		return s;
	}

	public static void main(String[] args) throws ClassNotFoundException, IOException {
		System.out.println("----开始构造Wrom");
		//构造6个蠕虫对象,且x起始为 A 
		Worm w = new Worm(6, 'A');
		System.out.println("w = " + w);

		System.out.println("----开始序列化");
		ByteArrayOutputStream bout = new ByteArrayOutputStream();
		ObjectOutputStream out2 = new ObjectOutputStream(bout);
		out2.writeObject("Worm storage\n");//存储一个字符串
		out2.writeObject(w);//存储Worm对象
		out2.flush();
		System.out.println("----开始反序列化");
		ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(bout
				.toByteArray()));
		String s = (String) in2.readObject();//读字符串
		Worm w3 = (Worm) in2.readObject();//读Worm对象
		System.out.println(s + "w3 = " + w3);
	}
}

 

某次运行的结果如下:

----开始构造Wrom
Worm constructor: 6
Worm constructor: 5
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worm constructor: 1
w = :A(6 8 7):B(9 2 1):C(4 7 8):D(4 1 7):E(4 0 8):F(9 7 3)
----开始序列化
----开始反序列化
Worm storage
w3 = :A(6 8 7):B(9 2 1):C(4 7 8):D(4 1 7):E(4 0 8):F(9 7 3)

使用Externalizable序列化

缺省的序列化机制并不难操作。然而,如果有特殊的需要那又该怎么办?例如,也许你有考虑特殊的安全问题,而且你不希望对象的某一部分被序列化;或者一个对象被重组以后,某子对象需要重新创建,从而不必将该子对象序列化。这时,可通过实现 Externalizable 接口代替实现 Serializable 接口来对序列化过程进行控制。这个 Externalizable 接口继承了 Serializable 接口 ,同时增添了两个方法:

readExternal(ObjectInput in) throws IOException,CalssNotFoundException

writeExternal(ObjectOutput out) throws IOException,CalssNotFoundException

这两个方法会在序列化和重组的过程中被自动调用,以便执行一些特殊操作。

 

Serializable 对象完全以它存储的二进制位为基础重组,反序列化时不会调用构造函数,哪怕是默认构造函数

 

而对一个 Externalizable 对象,反序列化时缺省构造函数先会被调用 ,然后调用 readExternal()

 

注:序列化方法(writeObject)不会自动对 transient 属性与静态的属性序列化 ( API ObjectOutputStream.  defaultWriteObject() 方法的描述就可知这个结论,其描述如下: Write the non-static and  non-transient fields of the current class to this stream.)

 

实现 Externalizable接口步骤 如下:

1 、实现 Externalizable 接口

2 、实现 writeExternal() ,在方法中指明序列化哪些对象,如果不存储则不能保存某属性状态

3 实现 readExternal() ,在方法中指明反序列化哪些对象,如果不读取则不能恢复某属性状态

 

Externalizable 恢复一个对象状态过程如下:

1 调用对象的缺省构造函数 ( 注:缺省构造函数一定要是 public ,其他都不行,否在反序列化时出错 )

2 、通过 readExternal() 对各个属性进行进一步的恢复

 

下面这个例子示范了如何完整保存和恢复一个Externalizable对象:

import java.io.Externalizable;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class Blip3 implements Externalizable {
	private int i;
	private String s; // 未初始化

	//Externalizable反序列化时会先调用
	public Blip3() {
		System.out.println("Blip3 Constructor");
		// s, i 未初始化
	}

	public Blip3(String x, int a) {
		System.out.println("Blip3(String x, int a)");
		s = x;
		i = a;
		// s & i 在非默认构造函数中初始化
	}

	public String toString() {
		return s + i;
	}

	public void writeExternal(ObjectOutput out) throws IOException {
		System.out.println("Blip3.writeExternal");
		// 序列化时你必须这样做,你不能((ObjectOutputStream) out).defaultWriteObject();这样
		out.writeObject(s);
		out.writeInt(i);

	}

	public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
		System.out.println("Blip3.readExternal");
		// 反序列化时你必须这样做,你不能((ObjectInputStream) in).defaultReadObject()这样
		s = (String) in.readObject();
		i = in.readInt();

	}

	public static void main(String[] args) throws IOException, ClassNotFoundException {
		System.out.println("--Constructing objects:");
		Blip3 b3 = new Blip3("A String ", 47);
		System.out.println(b3);
		ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("Blip3.out"));
		System.out.println("--Saving object:");
		o.writeObject(b3);
		o.close();
		// Now get it back:
		ObjectInputStream in = new ObjectInputStream(new FileInputStream("Blip3.out"));
		System.out.println("--Recovering b3:");
		b3 = (Blip3) in.readObject();
		System.out.println(b3);

	}
}

--Constructing objects:
Blip3(String x, int a)
A String 47
--Saving object:
Blip3.writeExternal
--Recovering b3:
Blip3 Constructor
Blip3.readExternal
A String 47

可控的 Serializable 序列化( Externalizable的替代方案)

有一种防止对象的敏感部分被序列化的办法,就是将我们自己的类实现为 Externalizable ,这样一来,没有任何东西可以自动序列化,并且我们可以 writeExteranl() 内部只对所需要部分进行显式的序列化。但是,如果我们正在操作的 确确实实是一个Serializable 对象,那么所有序列化操作都会自动进行。为了能够控制,可以用 transient ( 瞬时 ) 关键字逐个地关闭序列化 ,它意旨 " 不用麻你或恢复数据 -- 我自己会处理的 "

 

由于Externalizable对象在缺省情况下不保存它们的任何域,所以transient关键字只能和Serializable对象一起使用

 

如果我们不是特别想要实现 Externalizable 接口,那么就还有一种方法。我们可以实现 Serializable 接口,并添加 ( 注:我说的是 " 添加 " ,而不是 " 实现 " " 重载 ") 名为 writeObject() readObject() 的方法。这样一旦对象被序列化或都被反序列化,就会自动地分别调用这两个方法。也就是说,只要我们提供了这两个方法,就会使用它们而不是缺省的序列化机制 。这些方法必须具有准确的方法签名:

private void writeObject(ObjectOutputStream stream) throws IOException;

private void readObject(ObjectInputStream stream) throws IOException,CalssNot  FoundException

从设计的观点来看,现在事情变得真是不可思议。它们被定义成了 private ,这意思味着它们不能被这个类的其成员调用。然面,实际上我们并没有从这个类的其他方法中调用它们,而是 ObjectOutputStream ObjectInputStream 对象的 writeObject() readObject() 方法调用我们对象的 writeObject() readObject() 方法 在你调用 ObjectOutputStream.writeObject() 时,会检查你所传递的 Serializable 对象,看是否实现 ( 准确的说应该是添加 ) 了它自己的 writeObject() ,如果是这样,就跳过正常的序列化过程并调用它的 writeObject() readObject() 的情形与此相同。

 

还有另外一个技巧。在我们添加的 writeObject(ObjectOutputStream stream) 内部,可以调用 defaultWriteObject() 来选择执行缺省的 writeObject() 。类似地,在 readObject(ObjectInputStream stream) 内部,我们可以调用 defaultReadObject()

 

注:如果某实现了 Serializable 接口并添加了 writeObject() readObject() 方法的类,要保存非 transient 部分,那么我们必须调用 defaultWriteObject() 操作作为 writeObject() 中的第一个操作,并让 defaultReadObject() 作为 readObject() 中的第一个操作,如果不这样的话,我们只能手动一个个存储与恢复了

 

下面示例演于了如何对一个Serializable对象的存储与恢复进行控制:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * Serializable接口的自动序列化性为的控制
 */
public class SerialCtl implements Serializable {

	//非transient域可由defaultWriteObject()方法保存
	private String a;
	//transient域必须在程序中明确保存和恢复
	private transient String b;

	//默认构造函数,反序列化时不会调用
	public SerialCtl() {
		System.out.println("defuault constructor");
	}
	
	//构造函数,反序列化时不会调用
	public SerialCtl(String aa, String bb) {
		a = "Not Transient: " + aa;
		b = "Transient: " + bb;
	}

	public String toString() {
		return a + "\n" + b;
	}

	/* 
	 * 添加writeObject(ObjectOutputStream stream)私有方式,
	 * 序列化时会自动由ObjectOutputStream对象的writeObject方法来调用
	 */
	private void writeObject(ObjectOutputStream stream) throws IOException {
		//要在首行调用默认序列化方法
		stream.defaultWriteObject();
		//我们手工序列化那些调用默认序列化方法(defaultWriteObject)无法序列化的属性
		stream.writeObject(b);
	}

	/* 
	 * 添加readObject(ObjectInputStream stream)私有方式,
	 * 序列化时会自动由ObjectInputStream对象的readObject方法来调用
	 */
	private void readObject(ObjectInputStream stream) throws IOException,
			ClassNotFoundException {
		//要在首行调用默认序列化方法
		stream.defaultReadObject();
		//我们手工反序列化那些调用默认反序列化方法(defaultReadObject)无法反序列化的属性
		b = (String) stream.readObject();
	}

	public static void main(String[] args) throws IOException, ClassNotFoundException {
		SerialCtl sc = new SerialCtl("Test1", "Test2");
		System.out.println("Before:\n" + sc);
		ByteArrayOutputStream buf = new ByteArrayOutputStream();
		ObjectOutputStream o = new ObjectOutputStream(buf);
		o.writeObject(sc);
		// Now get it back:
		ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buf
				.toByteArray()));
		SerialCtl sc2 = (SerialCtl) in.readObject();
		System.out.println("After:\n" + sc2);

	}
}

Before:
Not Transient: Test1
Transient: Test2
After:
Not Transient: Test1
Transient: Test2

另一种可控的 Serializable 序列化

具体请参考《java解惑你知多少(七)》中的【54. 实现Serializable的单例问题】

同一对象多次序列化到同一输出流与不同输出流

如果我们将两个都具有指向第三个对象的引用的对象进行序列化,会发生什么情况?

当我们从它们的序列化状态恢复这两个对象时,第三个对象会只出现一次吗?

如果将这两个对象序列化成独立的文件,然后在代码的不同部分对它们进行反序列化,又会怎样呢?

请看本例分解:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

class House implements Serializable {
	private static final long serialVersionUID = 7763424872069972808L;
}

class Animal implements Serializable {
	private static final long serialVersionUID = 4585314037312913787L;
	private String name;
	private House preferredHouse;

	public Animal(String nm, House h) {
		name = nm;
		preferredHouse = h;

	}

	public String toString() {
		return name + "[" + super.toString() + "]," + preferredHouse + "\n";
	}

	public String getName() {
		return name;
	}

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

public class MyWorld {
	public static void main(String[] args) throws IOException, ClassCastException,
			ClassNotFoundException {
		House house = new House();
		List animals = new ArrayList();
		// 让三种动物都引用同一个对象house
		animals.add(new Animal("Bosco the dog", house));
		animals.add(new Animal("Ralph the hamster", house));
		animals.add(new Animal("Fronk the cat", house));

		System.out.println("animals:" + animals);

		System.out.println("----开始序列化");
		ByteArrayOutputStream buf1 = new ByteArrayOutputStream();
		ObjectOutputStream o1 = new ObjectOutputStream(buf1);
		o1.writeObject(animals);

		// 试着改变状态
		((Animal) animals.get(0)).setName("pig pig...");

		/*
		 * 再往同一输出流序列化一次,实质上上面的改变状态操作对这次序列化不会起作用,这里存储的
		 * 对象状态还是为第一次序列化时的属性那个状态,但在改变状态后,如果序列化到另一个输出流
		 * 中时,这时会以最新对象状态来序列化
		 */
		o1.writeObject(animals);//再存一次
		System.out.println(((Animal) animals.get(0)).getName());

		// 序列化到另外一个流中
		ByteArrayOutputStream buf2 = new ByteArrayOutputStream();
		ObjectOutputStream o2 = new ObjectOutputStream(buf2);
		o2.writeObject(animals);

		System.out.println("----开始反序列化");
		ObjectInputStream in1 = new ObjectInputStream(new ByteArrayInputStream(buf1
				.toByteArray()));
		ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(buf2
				.toByteArray()));
		/*
		 * 注,上面o1存储了两遍,现也同样从in1读取两遍,同一流中如果对同一对象存储多次后,
		 * 在读取时,也是同一对象
		 */
		List animals1 = (List) in1.readObject();
		List animals2 = (List) in1.readObject();

		// 如果输入到不同的流中时,尽管是存储的同一对象,但恢复过来时是不同的对象(内存地址不一样)
		List animals3 = (List) in2.readObject();

		System.out.println("animals1:" + animals1);
		System.out.println("animals2:" + animals2);
		System.out.println("animals3:" + animals3);
	}
}

animals:[Bosco the dog[序列化.Animal@d9f9c3],序列化.House@9cab16
, Ralph the hamster[序列化.Animal@1a46e30],序列化.House@9cab16
, Fronk the cat[序列化.Animal@3e25a5],序列化.House@9cab16
]
----开始序列化
pig pig...
----开始反序列化
animals1:[Bosco the dog[序列化.Animal@156ee8e],序列化.House@47b480
, Ralph the hamster[序列化.Animal@19b49e6],序列化.House@47b480
, Fronk the cat[序列化.Animal@10d448],序列化.House@47b480
]
animals2:[Bosco the dog[序列化.Animal@156ee8e],序列化.House@47b480
, Ralph the hamster[序列化.Animal@19b49e6],序列化.House@47b480
, Fronk the cat[序列化.Animal@10d448],序列化.House@47b480
]
animals3:[pig pig...[序列化.Animal@e0e1c6],序列化.House@6ca1c
, Ralph the hamster[序列化.Animal@1bf216a],序列化.House@6ca1c
, Fronk the cat[序列化.Animal@12ac982],序列化.House@6ca1c
]

结果:这些反序列化的对象地址与原来对象的地址肯定是不同。但请注意,在animals1和 animals2中却出现了相同的地址,包括两者共享的那个指向house的引用。另一方面 当恢复naimals3时,系统无法知道另一个流内的对象是第一个流内对象的别名,因此 它会产生出完全不同的对象网。


只要我们将任何对象序列化到一个单一流中,我们就可以恢复出与我们写出时一样的对象 网,并且没有任何意外重复复制出的对象。当然,我们可以在写出第一个对象与写出第二个对象期间改变这些对象的状态,这种更新操作对存储到同一流中的序列化操作不起作用; 只对更新状态后再存储到另一流中起作用

 

ObjectOutputStream.writeUnshared

ObjectOutputStream.writeUnshared:

 将“未共享”对象写入 ObjectOutputStream。此方法等同于 writeObject,不同点在于它总是将给定对象作为流中惟一的新对象进行写入(相对于指向以前序列化实例的 back 引用而言)。尤其是:

  • 通过 writeUnshared 写入的对象总是作为新出现对象(未曾将对象写入流中)被序列化,不管该对象以前是否已经被写入过。
  • 如果使用 writeObject 写入以前已经通过 writeUnshared 写入的对象,则可将以前的 writeUnshared 操作视为写入一个单独对象,即writeObject 会重新生成一个新的对象。换句话说,ObjectOutputStream 永远不会生成通过调用 writeUnshared 写入的对象数据的 back 引用。

虽然通过 writeUnshared 写入对象本身不能保证反序列化对象时对象引用的惟一性,但它允许在流中多次定义单个对象,因此接收方对 readUnshared 的多个调用不会引发冲突。注意,上述规则仅应用于通过 writeUnshared 写入的基层对象(被序列化对象本身),而不能应用于要序列化的对象图形中的任何可变迁方式引用的子对象(即不会再次创建被序列化对象里的成员对象)。

 

public class Test {
	private final static class V implements Serializable {
		StringBuffer sb = new StringBuffer();
	};

	private static V v = new V();

	public static void main(String[] args) throws Exception {
		testWriteUnshared(3);
	}

	static void testWriteUnshared(int times) throws Exception {
		ByteArrayOutputStream bos1,bos2;
		ObjectOutputStream oos1,oos2;
		ByteArrayInputStream bin1,bin2;
		ObjectInputStream ois1,ois2;

		bos1 = new ByteArrayOutputStream();
		oos1 = new ObjectOutputStream(bos1);
		bos2 = new ByteArrayOutputStream();
		oos2 = new ObjectOutputStream(bos2);
		for (int i = 0; i < times; i++) {
			v.sb.append(i);
			/*
			 * 将同一个对象存储到同一流时,writeUnshared只是对v本身进行了多个复制,但
			 * 对象里所引用的其他成员对象如这里的sb是不会再次复制的,它会引用第一次向该
			 * 流写入时的同一个对象,所以当一个对象内部状态改变后通writeUnshared写入还
			 * 是不能更新,它只不过写入了另一个基层对象而已。
			 */
			oos1.writeUnshared(v);
			//将同一对象写入同一流时,对象不会重新写入,而是引用第一次序列化后的对象
			oos2.writeObject(v);
		}

		bin1 = new ByteArrayInputStream(bos1.toByteArray());
		ois1 = new ObjectInputStream(bin1);
		bin2 = new ByteArrayInputStream(bos2.toByteArray());
		ois2 = new ObjectInputStream(bin2);
		V v = null;
		for (int i = 0; i < times; i++) {
			v = (V) ois1.readUnshared();
			/*
			 * 某次输出结果:
			 * Test$V@a62fc3 : 0  9023134
			 * Test$V@1270b7 : 0  9023134
			 * Test$V@60aeb0 : 0  9023134
			 */
			System.out.println(v + " :- " + v.sb + "  " + v.sb.hashCode());
			
			v = (V) ois2.readObject();
			/*
			 * 某次输出结果:
			 * Test$V@60aeb0 : 0  23899971
			 * Test$V@60aeb0 : 0  23899971
			 * Test$V@60aeb0 : 0  23899971
			 */
			System.out.println(v + " : " + v.sb + "  " + v.sb.hashCode());
		}
	}
}

 

 

静态与transient数据不可序列化

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class A implements Serializable {
	private static final long serialVersionUID = -4829544934963584924L;
	public static int i = prt();
	public transient int y = 33;
	public int j = prt(22);

	//Serializable方式序列化时是不会调用构建函数的
	public A() {
		System.out.println("调用构建");
	}

	public static int prt() {
		System.out.println("初使化静态变量i");
		return 1;
	}

	public static int prt(int j) {
		System.out.println("初使化变量j");
		return j;
	}

}

public class StaticTransientFeildSerial {

	public static void main(String[] args) throws FileNotFoundException, IOException,
			ClassNotFoundException {
		if (args.length == 0) {
			// 开始序列化:
			ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(
					"serial.dat"));
			A.i = 2;
			out.writeObject(new A());
			out.close();
		} else {
			// 反序列化:
			ObjectInputStream in = new ObjectInputStream(
					new FileInputStream("serial.dat"));

			// 默认恢复操作时,如果恢复的类在恢复前未加载进来过,则在恢复时会以定义时的值初始化
			// (如果值是调用某方法获得的,则也会去调用初始化方法),如果加载过,则对它不进行任何
			// 操作,其实这是类加载时对静态成员变量的初始化机制罢了。
			A a = (A) in.readObject();
			System.out.println("非静态变量j=" + a.j);
			System.out.println("静态变量i=" + A.i);
			System.out.println("transient变量y=" + a.y);
			in.close();
		}
	}
}

序列化运行结果:

初使化静态变量i
初使化变量j
调用构建


反序列化运行结果:

初使化静态变量i
非静态变量j=22 
      //注:可以看到在恢复非静态变量j时没有调用prt(22)方法,但状态已恢复
静态变量i=1            //说明静态变量没能恢复过来,如果要恢复静态变量的状态只能手工序列化
transient变量y=0    //说明transient变量没能恢复过来,如果要恢复静态变量的状态只能手工序列化

 

所以,如果要对静态成员与transient成员进行序列化时,我们只能通 Externalizable 或者是可控的 Serializable 来实现。

 

哪此属性不会被序列化?

并不是一个实现了序列化接口的类的所有字段及属性都是可以序列化的:

  • 如果该类有父类,则分两种情况来考虑,如果该父类已经实现了可序列化接口。则其父类的相应字段及属性的处理和该类相同;如果该类的父类没有实现可序列化接口,则该类的父类所有的字段属性将不会序列化,并且反序列化时会调用父类的默认构造函数来初始化父类的属性,而子类却不调用默认构造函数,而是直接从流中恢复属性的值。
  • 如果该类的某个属性标识为static类型的,则该属性不能序列化。
  • 如果该类的某个属性采用transient关键字标识,则该属性不能序列化。

序列化类多重版本的控制

如果在反序列化的JVM 里出现了该类的不同时期的版本,那么我们该如何处理的呢?


为了避免这种问题,Java的序列化机制提供了一种指纹技术,不同的类带有不同版本的指纹信息,通过其指纹就可以辨别出当前JVM 里的类是不是和将要反序列化后的对象对应的类是相同的版本。该指纹实现为一个64bit的long 类型。通过安全的Hash算法(SHA-1)来将序列化的类的基本信息(包括类名称、类的编辑者、类的父接口及各种属性等信息)处理为该64bit的指纹。我们可以通过JDK自带的命令serialver来打印某个可序列化类的指信息。如下:
E:\Test\src>serialver serial.SerialClass
serial.SerialClass: static final long serialVersionUID = -5764322004903657926L;


问题的出现 :如果经过多次修改,会得到不同指纹信息,当一个指纹信息已变化的序列化对象在另一虚拟机反序列化时,由于另一虚拟机上类的指纹信息与反序列化对象的指纹信息不同,所以在反序列会过程中会出现异常。下面我们来做一个实验:
1、比如有这样一个类:

public class SerialClass implements Serializable {
    public String firstName;
}

2、现在我们把它序列化到一个文件中,代码如下:

public void testSerial() {
    try {
        SerialClass sc = new SerialClass();
        sc.firstName = "j";
        ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("serail.dat"));
        oos.writeObject(sc);
        oos.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

3、现在我们来修改SerialClass,增加一个字段:

public class SerialClass implements Serializable {
    public String firstName;
    public String lastName;
}

4、现在我们来反序列化,使用如下代码:

public void testDeSerial() {
    ObjectInputStream ois;
    try {
        ois = new ObjectInputStream(new FileInputStream("serail.dat"));
        SerialClass sc = (SerialClass) ois.readObject();
        System.out.println(sc.firstName);
        ois.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

5、运行时抛如下异常:
java.io.InvalidClassException: serial.SerialClass; local class incompatible: stream classdesc serialVersionUID = -7303985972226829323, local class serialVersionUID = -5764322004903657926


从上面异常可以看出是由于类被更新,导致指纹信息发生变化反序列化时出错

解决办法 :在需要序列化SerialClass类加上 private static final long serialVersionUID 版本属性,并且值为以修改前的指纹值-7303985972226829323L,这样在编译时就不会自动要所代码来生成指纹信息了,而是做我们指定的指纹。修改后代码如下:

public class SerialClass implements Serializable {
    private static final long serialVersionUID = -7303985972226829323L;
    public String firstName;
    public String lastName;
}

现在我们再来反一把,结果正常。

结论 在我们实现Serializable接口时一定要指定版本信息属性 serialVersionUID ,这样在我们修改类后,指纹信息不会发生改变,使用修改过的类反序列化时兼容对以前创建的序列化对象。

你可能感兴趣的:(jvm,算法,虚拟机,网络应用,J#)