Java序列化与反序列化

本文深入探讨Java对象序列化机制,介绍序列化的重要性、应用场景及其实现方式,包括JSON转换、ProtoBuf工具和Java内置序列化API。同时,解析序列化与反序列化的过程,以及如何通过transient关键字和自定义序列化方法精确控制属性的序列化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

Java对象是在JVM中生成的,若需要远程传输或保存到硬盘上,需将Java对象转换成可传输的文件流。几种转换方式:
方式一
利用Java的序列化机制将对象序列化成字节,一般是需要加密传输时才用。
方式二
将对象包装成json字符串,转json的工具有FastJson、Jackson和GJson,各有优缺点
FastJson:速度最快。将复杂类型的Bean转换成json可能会出问题,适合对性能有要求的场景。
Jackson:Map、List的转换可能会出现问题。转换复杂类型的Bean时,转换的json格式不是标准的json格式,适合处理大文本json。
GJson:功能最全。可将复杂的Bean对象和json字符串进行互转。性能上对比FastJson有所差距,适合处理小文本json,和对数据正确性有要求的场景。
方式三
protoBuf工具(转换成二进制)
性能好,效率高,字节数很小,网络传输节省IO,但二进制可读性差。

概念

Java序列化是指把Java对象转换成字节序列的过程;反序列化是指把字节序列恢复为Java对象的过程。

为何需要序列化与反序列化

当两个进程进行远程通信时,可相互发送各种类型的数据,包括文本、图片、音频、视频等,这些数据都会以二进制序列的形式在网络上传送。两个Java进程进行通信时,也可以实现进程间的对象传送,此过程就需要序列化与反序列化。一方面,发送方需要将Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。
由此可看到Java序列化的好处,其一是可以实现数据持久化,通过序列化的方式将数据保存到硬盘上(通常是文件);其二是利用序列化实现远程通信,即在网络上传送对象的字节序列。

如何实现Java的序列化与反序列化

JDK类库中序列化API
java.io.ObjectOutputStream:表示对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
java.io.ObjectInputStream:表示对象输入流,它的readObject()方法从输入流中读取字节序列,再把它们反序列化成一个对象,并将其返回。

实现序列化的要求

想要类的对象能够被序列化,该类必须实现Serializable接口或Externalizable接口,否则会抛出异常。

实现Java对象序列化与反序列化的方法

假定一个Person类,它的对象需要序列化,有如下三种方法:
方法一
Person类仅实现了Serializable接口,ObjectOutputStream采用默认序列化的方式,对Person对象的非static、transient的实例变量进行序列化;ObjectInputStream采用默认反序列化的方式,对Person对象的非static、transient的实例变量进行反序列化。
方法二
若Person类实现了Serializable接口,而且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputStream)
ObjectOutputStream调用Person对象的writeObject(ObjectOutputStream out)进行序列化,
ObjectInputStream调用Person对象的readObject(ObjectInputStream in)进行反序列化。
方法三
若Person实现了Externalizable接口,那么Person类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法
ObjectOutputStream调用Person对象的writeExternal(ObjectOutput out)进行序列化,
ObjectInputStream调用Person对象的readExternal(ObjectInput in)进行反序列化。
选择序列化transient
当对某个对象进行序列化时,系统会自动将该对象的所有属性依次进行序列化,若某个属性引用到另一个对象,则被引用的对象也会被序列化。若被引用的对象也有属性引用了其他对象,则那个对象也会被序列化。并且若要序列化的对象所在的类存在父类时,其父类也必须被序列化。这就是递归序列化。
有时候,并不希望出现递归序列化,或是存在某个敏感信息(如密码)的属性不被序列化,就可通过transient关键字来修饰该属性来阻止被序列化。如:
transient private String password;
JDK类库中序列化的步骤
步骤一:创建一个对象输出流,它可包含一个其他类型的目标输出流,如文件输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(“D:\JavaTest\test.txt”));
步骤二:通过对象输出流的writeObject(Object obj)方法序列化对象
oos.writeObject(person1);
JDK类库中反序列化的步骤
步骤一:创建一个对象输入流,它可包含一个其他类型的输入流,如文件输入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(“D:\JavaTest\test.txt”));
步骤二:通过对象输入流的readObject()方法反序列化成对象
Person person2 = (Person)ois.readObject();

方法一实现序列化与反序列化实例

定义Person类,让其实现Serializable接口

public class Person implements Serializable {
	//显式声明版本序列号
	private static long seriaVersionUID = 1L;
	private String name;
	private Integer age;
	private String gender;
	//省略getter和setter方法,无参构造和带参构造
}

创建测试类

public class Test {
	public static void main(String[] args) {
		Person person1 = new Person("成颖","20","女");
		Person person2 = null;
		File file = new File("D:\\JavaTest\\test.txt");
		try {
			//序列化
			ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
			oos.writeObject(person1);
			oos.flush();
			oos.close();

			//反序列化
			ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
			person2 = (Person)ois.readObject();
			ois.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println("姓名:" + person2.getName());
		System.out.println("年龄:" + person2.getAge());
		System.out.println("性别:" + person2.getGender());
	}
}

打印结果为
姓名:成颖
年龄:20
性别:女

反序列化的时候没有调用Person类的任何构造方法,这就给实现了序列化机制的单例类造成了麻烦。详细介绍可以去看我写的单例模式那篇文章:
设计模式(三)----单例模式
对象的类名、属性都会被序列化,方法、static属性、transient属性都不会被序列化(虽加static也可阻止该属性被序列化,但static关键字不是这么用滴)。
反序列化读取的仅仅是Java对象的数据,而不是Java类,所以在反序列化时必须提供该Java对象所属类的class文件(很多没有被序列化的数据需要从class文件中获取,如上面说的static、transient属性,方法等),否则会报ClassNotFoundException异常。
当通过文件、网络来读取序列化后的对象时,须按实际的写入顺序读取。

方法二实现序列化与反序列化

Person类中其他不变,加入下面两个方法

//自定义序列化方法
private void writeObject(ObjectOutputStream oos) throws IOException {
	//将当前类的非静态和非瞬时态属性写入该流,若不写,若还有其他属性,则不会被序列化
	//oos.defaultWriteObject();
	//将name简单加密(反转),实际中当然不可能这样加密
	oos.writeObject(new StringBuffer(name).reverse());
	oos.writeInt(age);
}
//自定义反序列化方法
private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException {
	//从该流中读取该类的非静态和非瞬时态属性,若不写,其他属性就不能被反序列化
	//ois.defaultReadObject();
	//解密
	((StringBuffer)ois.readObject()).reverse().toString();
}

用相同的测试类测试,打印结果为
姓名:成颖
年龄:null
性别:null
由于自定义序列化方法中只对name和age属性进行了序列化,自定义反序列化方法中只对name属性进行了反序列化,所以只有name属性正常获取,age和gender属性都为null。
说明
使用transient关键字阻止属性序列化虽然简单方便,但被其修饰的属性就被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性值,而通过在序列化的对象的类中加入writeObject(ObjectOutputStream oos)和readObject(ObjectInputStream ois)方法可对各个属性的序列化和反序列化控制的更加精确。

方法三实现序列化与反序列化

实现该接口与实现Serializable接口类似,只是实现该接口的类必须强制自定义序列化(重写writeExternal(ObjectOutput oo)和readExternal(ObjectInput oi)方法)。
定义Person2类,让其实现Externalizable接口

public class Person2 implements Externalizable {
	private String name;
	private Integer age;
	private String gender;
	//省略无参构造、带参构造和getter、setter方法
	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		//将name简单加密
		out.writeObject(new StringBuffer(name).reverse());
		out.writeInt(age);
	}
	@Override
	public class readExternal(ObjectInput in) throws IOException,ClassNotFoundException {
		//解密
		name = ((StringBuffer)in.readObject()).reverse().toString();
		age = in.readInt();
	}
}

用相同的测试类测试,打印结果为
姓名:成颖
年龄:20
性别:null
可以看到,只获取到了name和age属性,那是因为自定义序列化和反序列化方法中没有对gender属性进行序列化和反序列化。
注:实现External接口的方式将序列化和反序列化的工作完全交给了开发人员,那样的好处就是自由度变大,序列化和反序列化的过程更加灵活,但对开发人员的技术能力也有了一个更高的要求。

writeReplace()和readResolve()方法

通过writeReplace()方法控制序列化过程,为实现了Serializable接口的类提供如下签名方法:
Object writeReplace() throws ObjectStreamException;
该方法在序列化方法writeObject()之前执行,所以可以在序列化之前对要序列化的对象做一些处理,甚至完全替换掉原来的对象如下面的代码无论被序列化的对象是什么,反序列化出来的对象总是一个字符串"嘿嘿":

private Object writeReplace() throws ObjectStreamException {
	return "嘿嘿";
}

通过readResolve()方法控制反序列化过程,为实现了Serializable接口的类提供如下签名方法:
Object readResolve() throws ObjectStreamException;
该方法在反序列化方法readObject()之后执行,所以可以在反序列化之后对获得的对象做一些处理,甚至完全替换为其它对象。如下面的代码无论反序列化得到的对象是什么,都会被替换为一个字符串"哈哈":

private Object readResolve() throws ObjectStreamException {
	return "哈哈";
}

这个方法在单例类实现序列化时特别有用,因为通过序列化和反序列化可以不通过构造方法来获取一个类的实例,这样就容易破坏单例。

seriaVersionUID

若seriaVersionUID没有显式声明,系统就会自动生成一个。此时,若对象序列化后对该对象所属的类做加属性或减属性的操作,系统在反序列化时会重新生成一个seriaVersionUID然后去和已经序列化的对象进行比较,就会报序列号版本不一致的错误。为避免这种问题,一般系统会要求实现了Serializable接口的类显式的声明一个seriaVersionUID。
所以显式定义seriaVersionUID有两种用途:
希望类的不同版本对序列化兼容时,需确保类的不同版本具有相同的seriaVersionUID。
不希望类的不同版本对序列化兼容时,需确保类的不同版本具有不同的seriaVersionUID。
应该总是显式的指定一个版本号,这样不仅可增强对序列化版本的控制,还提高了代码的可移植性。因为不同的JVM可能会使用不同的策略来计算这个版本号,那样的话,同一个类在不同JVM下也会被认为是不同的版本。
如何维护版本号
只修改了类的方法、被static和transient修饰的属性时无需修改seriaVersionUID,新增或删除类中的属性时需修改seriaVersionUID。

序列化机制算法

所有保存的磁盘中的对象都有一个序列化编号。
当程序试图序列化一个对象时,会先检查该对象是否已经被序列化过。若未被序列化过,程序会将该对象转换成字节序列并输出;若已被序列化过,将直接输出一个序列化编号。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值