详解java对象序列化

java序列化是指将对象转为字节流保存下来,并在以后还原这个对象的过程。

序列化是将对象存储下来(如存到文件中),因为java虚拟机一关闭存在内存中的new出来的对象就没了,反序列化是指将这个对象从文件加载回内存中。

将对象保存到永久存储设备上称为持久化。

采用反序列化恢复java对象时,必须提供java对象所属的class文件,否则会引发classNotFoundException异常。即,将一个序列化对象写入文件后,若要打开文件并读取该对象中的内容,必须要找到该对象的class对象,例如对象A,必须保证java虚拟机能找到A.class文件。

一、默认序列化机制(Serializable)

序列化某个对象,则该对象对应的类实现Serializable接口,可使用ObjectOutputStream(FileOutputStream(file))写进文件,或使用ObjectOutputStream(ByteArrayOutputStream)写进字节数组流,读对应ObjectInputStream。

Java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据,利用对象序列化可以进行对象的“深复制”,即复制对象本身及引用的对象本身。

在对一个Serializable对象进行还原的过程中,没有调用任何构造器,包括默认的构造器,对象完全以它存储的二进制位为基础来构造。

将对象写入流中时,每个序列化对象的类都会被编码,编码内容包括类名、类签名以及非瞬态(transient)和非静态(static)字段的值、其他对象的引用(瞬态和静态字段除外),读取对象也同理。

对一个对象进行序列化,不仅该对象要实现Serializable接口,其成员变量也要实现Serializable接口,否则会在运行期间抛出NotSerializableException。如下例中Data类也要实现Serializable接口。

例子:

import java.io.*;
import java.util.*;

class Data implements Serializable{
	private int n;
	public Data(int n) { this.n = n; }
	public String toString() { return Integer.toString(n); }
}

public class Worm implements Serializable {
	private static Random rand = new Random(50);
	private Data[] d = {
		new Data(rand.nextInt(10)),
		new Data(rand.nextInt(10)),
		new Data(rand.nextInt(10))
	};
	private Worm next;
	private char c;
	public Worm(int i, char x) {
		System.out.println("Worm constructor");
		c = x;
		if(--i > 0)
			next = new Worm(i, (char)(x + 1));
	}
	public Worm() {
		System.out.println("Default constructor");
	}
	public String toString() {
		StringBuilder result = new StringBuilder(":");
		result.append(c);
		result.append("(");
		for(Data data : d)
			result.append(data);
		result.append(")");
		if(next != null)
			result.append(next);
		return result.toString();
	}
	public static void main(String[] args) throws ClassNotFoundException, IOException {
		Worm w = new Worm(6, 'a');
		System.out.println("w=" + w);
		//写进文件
		ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("worm.out"));
		out.writeObject("Worm storage\n");
		out.writeObject(w);
		out.close();
		ObjectInputStream in = new ObjectInputStream(new FileInputStream("worm.out"));
		String s = (String)in.readObject();
		Worm w2 = (Worm)in.readObject();
		System.out.println(s + "w2=" + w2);
		//写进字节数组
		ByteArrayOutputStream bout = new ByteArrayOutputStream();
		ObjectOutputStream out2 = new ObjectOutputStream(bout);
		out2.writeObject("Worm storage\n");
		out2.writeObject(w);
		out2.flush();
		ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray()));
		s = (String)in2.readObject();
		Worm w3 = (Worm)in2.readObject();
		System.out.println(s + "w3=" + w3);
	}
}
输出结果如下:

Worm constructor
Worm constructor
Worm constructor
Worm constructor
Worm constructor
Worm constructor
w=:a(783):b(211):c(686):d(802):e(058):f(227)
Worm storage
w2=:a(783):b(211):c(686):d(802):e(058):f(227)
Worm storage
w3=:a(783):b(211):c(686):d(802):e(058):f(227)

可以看到Worm对象及其Data对象中的数据都被保存下来了。

特殊情况:父类不可序列化而子类可序列化:

要想将父类也序列化,则父类必须实现Serializable接口,否则虚拟机是不会序列化父类对象的。如果只想序列化子类,则父类要有默认的无参构造函数,这是因为一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。

例子:

import java.io.*;
import java.util.*;

class Father{
	String c;
	public Father() { //必须的,否则会抛InvalidClassException
	}

	public Father(String c) {
		this.c = c;
	}

	public String toString() {
		return "c=" + c;
	}
}

public class Worm extends Father implements Serializable {
	private String a,b;
	public Worm(String a, String b, String c) {
		super(c);
		this.a = a;
		this.b = b;
	}
	public String toString() {
		return "a=" + a + ",b=" + b + ",c=" + c;
	} 
	public static void main(String[] args) throws Exception{
		Worm w = new Worm("zheng","jun","yan");
		System.out.println("w:" + w);
		ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("data.txt"));
		o.writeObject(w);
		ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.txt"));
		Worm w1 = (Worm)in.readObject();
		System.out.println("w1:" + w1);
	}
}
输出结果为:

w:a=zheng,b=jun,c=yan
w1:a=zheng,b=jun,c=null

若Father类实现Serializable接口,则输出结果为:

w:a=zheng,b=jun,c=yan
w1:a=zheng,b=jun,c=yan

若去掉Father的默认构造函数,则会在运行时抛出InvalidClassException异常。

关于静态变量序列化的一点纠结问题:

正如前面提到的,static和transient字段不会被序列化,请看下面例子:

import java.io.*;
import java.util.*;

public class Test implements Serializable {
	private String a;
	private transient String b;
	private static String c;
	public Test(String a, String b, String c) {
		this.a = "Not transient: " + a;
		this.b = "Transient: " + b;
		this.c = "static: " + c;
	}
	/*public void setC(String c1) {
		c = c1;
	}*/
	public String toString() {
		return a + "\n" + b + "\n" + c;
	}
	public static void main(String[] args) throws IOException, ClassNotFoundException {
		Test t1 = new Test("Test1", "Test2", "Test3");
		System.out.println("Before:\n" + t1);
		ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data.txt"));
		out.writeObject(t1);
		out.close();
		//t1.setC("ccc");
		ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.txt"));
		Test t2 = (Test)in.readObject();
		in.close();
		System.out.println("After:\n" + t2);
	}
}
可看到输出结果如下:

Before:
Not transient: Test1
Transient: Test2
static: Test3
After:
Not transient: Test1
null
static: Test3

transient字段按我们期望的没有被序列化,读出后结果为null,但static字段结果显示却与写入的一样,为Test3。原因是静态字段是与类相关的,而不是与对象相关的,在序列化对象的时候,静态字段并没有被序列化,但初始化操作使得该类的静态变量c="Test3",在反序列化时读到的是静态变量回去引用类的值,所以最后为"Test3"。

若将上面代码的注释部分去掉,可以看到如下结果:

Before:
Not transient: Test1
Transient: Test2
static: Test3
After:
Not transient: Test1
null
ccc

出现了有点诡异的现象,虽然static字段c是在将对象写入文件后才被设置为"ccc"的,但将对象读出后发现c已变为"ccc"了,这就验证了上述结论,即静态变量不会被序列化,但反序列化重构对象时会读到改后的静态变量的值。

下面看另一个让人纠结的现象:

若将上面例子分为两个程序来执行,一个负责写对象,一个负责读对象,如

程序1:

import java.io.*;
import java.util.*;

public class Test3 implements Serializable {
	private String s;
	private transient String b;
	private static String c;
	public Test3(String s, String b, String c) {
		this.s = "Not transient: " + s;
		this.b = "Transient: " + b;
		this.c = "static: " + c;
	}
	public String toString() {
		return s + "\n" + b + "\n" + c;
	}
	public static void main(String[] args) throws IOException, ClassNotFoundException {
		Test3 sc = new Test3("Test1", "Test2","Test3");
		System.out.println("Before:\n" + sc);
		ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data.txt"));
		out.writeObject(sc);
		out.close();
	}
}
程序2:

import java.io.*;
public class Test4 {
	public static void main(String[] args) throws Exception {
		ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.txt"));
		Test3 sc1 = (Test3)in.readObject();
		in.close();
		System.out.println("After:\n" + sc1);
	}
}
先运行第一个程序,得到输出结果:

Before:
Not transient: Test1
Transient: Test2
static: Test3

再运行第二个程序,得到输出结果:

After:
Not transient: Test1
null
null

读到的对象中静态字段c居然是null了!也就是说,如果将写入跟读取对象放在同一个程序中,最终得到的静态变量会是最终赋给它的值,但是如果将写入跟读取分别放在两个程序中执行,最终得到的静态变量就变为null了!

其实如果理解了上面那段结论,要解释这个现象也不难。静态成员变量在类装载的时候就进行了创建,在整个程序结束时按序销毁,因此,当执行完第一个程序后,静态变量随着程序的结束而被销毁,所以在执行第二个程序中的读取对象操作时,反序列化对象读到的静态变量就为null了。

二、实现Externalizable接口

有特殊需要时,如不希望对象的某一部分被序列化,或者不希望某个对象的子对象被序列化等,可通过实现Externalizable接口来替代实现Serializable接口,对序列化过程进行控制。

将类实现为Externalizable后,没有任何东西可以自动序列化,并且可以在writeExternal方法中对所需部分进行显示的序列化。

Externalizable接口继承了Serializable接口,并添加了两个方法:writeExternal(ObjectOutput out)和readExternal(ObjectInput in)方法,这两个方法会在序列化和反序列化还原的过程中被自动调用。

在重构 Externalizable 对象时,先使用无参数的公共构造方法创建一个实例,然后调用 readExternal 方法。通过从 ObjectInputStream 中读取 Serializable 对象可以恢复这些对象。因此,对于一个Externalizable对象,所有普通的默认构造器都会被调用(包括在字段定义时的初始化),然后调用readExternal()方法,因此默认构造器要设为public。在反序列化过程中,带参数的构造器不会被调用,因此在带参数构造器中所有初始化操作也就不会被保存,所以要在readExternal方法中重新初始化。

例子:

import java.io.*;

public class Blip implements Externalizable {
	private int i;
	private String s;
	public Blip() {
		System.out.println("Blip defaultConstructor");
	}
	public Blip(String s, int i) {
		System.out.println("Blip(int i, String s)");
		this.i = i;
		this.s = s;
	}
	public String toString() {
		return s + i;
	}
	public void writeExternal(ObjectOutput out) throws IOException{
		System.out.println("Blip.writeExternal");
		out.writeObject(s);
		out.writeInt(i);
	}
	public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
		System.out.println("Blip.readExternal");
		s = (String)in.readObject();//重新初始化
		i = in.readInt();
	}
	public static void main(String[] args) throws IOException, ClassNotFoundException {
		System.out.println("Constructing object:");
		Blip b = new Blip("A String ", 50);
		System.out.println(b);
		ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("Blip.out"));
		System.out.println("Saving object:");
		o.writeObject(b);
		o.close();
		ObjectInputStream in = new ObjectInputStream(new FileInputStream("Blip.out"));
		Blip bb = (Blip)in.readObject();
		System.out.println("Recovering b:");
		System.out.println(bb);
	}
}
输出结果如下:

Constructing object:

Blip(int i, String s)
A String 50
Saving object:
Blip.writeExternal
Blip defaultConstructor
Blip.readExternal
Recovering b:
A String 50

可以看到整个序列化过程是:先写对象,再调用默认构造器创建实例,再反序列化对象。

三、Externalizable的替代方法

如果实现Serializable接口,并添加自己的writeObject和readObject方法,就可以对序列化进行控制,这两个方法必须按照下面形式定义:

Private void writeObject(ObjectOutputStream stream) throws IOException;

Private void readObject(ObjectInputStream stream) throws IOException,ClassNotFoundException;

注意到这两个方法都为private,因为在接口中定义的方法都为public,因此这两个方法不是接口的一部分。当调用ObjectOutputStream.writeObject(s)时,会检查所传递的Serializable对象s是否有自己的writeObject方法,如果有,这跳过正常的序列化过程并调用它的writeObject方法,readObject同理。

那么,ObjectOutputStream.writeObject方法是如何判断对象s中是否有自定义的writeObject方法的,这是利用反射来搜索的。

例子:
import java.io.*;

public class SerialCtl implements Serializable {
	private String s;
	private transient String b;
	public SerialCtl(String s, String b) {
		this.s = "Not transient: " + s;
		this.b = "Transient: " + b;
	}
	public String toString() {
		return s + "\n" + b;
	}
	private void writeObject(ObjectOutputStream stream) throws IOException {
		stream.defaultWriteObject();
		stream.writeObject(b);
	}
	private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
		stream.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 out = new ObjectOutputStream(buf);
		out.writeObject(sc);
		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

可以看到我们可以按自己的方式来序列化对象,首先调用默认序列化方法序列化对象,此时定义为transient的字段b将不会被序列化,然后我们再手动将b序列化,可看到最终结果两个字段都被保存下来了。


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值