Java面向对象系列[v1.0.0][自定义序列化]

自定义序列化

当一个类里包含的某些实例变量是敏感信息,这个时候不希望系统将该实例变量进行序列化,或者某个实例变量的类型是不可序列化的,因此不希望对该实例变量进行递归序列化,从而避免报java.io.NotSerializableException

当对某个对象进行序列化时,系统会自动把该对象的所有实例变量依次进行序列化,如果某个实例变量引用到另一个对象,则被引用的对象也会被序列化,如果被引用的对象的实例变量引用了其他对象,则被应用的对象也会被序列化,这种情况称为递归序列化

transient关键字

序列化

通过在实例变量前使用transient关键修饰,告诉Java序列化的时候无需理会该实例变量,如下代码所示:

public class Person
	implements java.io.Serializable
{
	private String name;
	private transient int age;
	// 注意此处没有提供无参数的构造器!
	public Person(String name, int age)
	{
		System.out.println("有参数的构造器");
		this.name = name;
		this.age = age;
	}
	// 省略name与age的setter和getter方法

	// name的setter和getter方法
	public void setName(String name)
	{
		this.name = name;
	}
	public String getName()
	{
		return this.name;
	}

	// age的setter和getter方法
	public void setAge(int age)
	{
		this.age = age;
	}
	public int getAge()
	{
		return this.age;
	}
}

transient关键字只能用于修饰实例变量

反序列化

import java.io.*;

public class TransientTest
{
	public static void main(String[] args)
	{
		try (
			// 创建一个ObjectOutputStream输出流
			var oos = new ObjectOutputStream(new FileOutputStream("transient.txt"));
			// 创建一个ObjectInputStream输入流
			var ois = new ObjectInputStream(new FileInputStream("transient.txt")))
		{
			var per = new Person("孙悟空", 500);
			// 系统会per对象转换字节序列并输出
			oos.writeObject(per);
			// 从序列化文件中读取该Person对象
			var p = (Person) ois.readObject();
			// 输入该Person对象的age实例变量值,输入0,因为age被transient关键字修饰
			System.out.println(p.getAge());
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
}

自定义序列化I

使用transient修饰的实例变量被完全隔离在序列化机制以外,反序列化的时候完全无法获取,除了这种情况外Java还提供了一种自定义序列化机制,通过这种自定义序列化机制可以让程序控制如何序列化各实例变量,甚至完全不序列化某些实例变量,需要做这种特殊处理的类必须提供如下特殊签名方法,这些方法用于实现自定义序列化

  • private void writeObject(java.io.ObjectOutputStream out)throws IOException: 负责写入特定类的实力状态,从而相应的readObject()方法可以恢复它,通过重写该方法,程序员可以完全获得对序列化机制的控制,可以自主决定哪些实例变量需要序列化,需要如何序列化,默认情况下,该方法会调用out.defaultWriteObject来保存Java对象的各实例变量,从而可以实现序列化Java对象
  • private void readObject(java.io.ObjectInputStream in)throws IOException, ClassNotFoundException: 负责从流中读取并恢复对象实例变量,通过重写该方法,可以完全获得对反序列化机制的控制,可以自主决定需要反序列化哪些实例变量,以及如何进行反序列化,默认情况下,该方法会调用in.defaultReadObject来恢复Java对象的没有被transient修饰的实例变量。通常情况下readObject()方法与writeObject()方法对应,如果writeObject()方法中对Java对象的实例变量进行了一些处理,则应该在readObject()方法中对其实例变量进行相应的饭处理从而正确恢复该对象
  • private void readObjectNoData()throws ObjectStreamException: 当序列化流不完整时,readObjectNoData()方法可以用来正确地初始化反序列化对象
import java.io.*;

public class Person
	implements java.io.Serializable
{
	private String name;
	private int age;
	// 注意此处没有提供无参数的构造器!
	public Person(String name, int age)
	{
		System.out.println("有参数的构造器");
		this.name = name;
		this.age = age;
	}
	// 省略name与age的setter和getter方法

	// name的setter和getter方法
	public void setName(String name)
	{
		this.name = name;
	}
	public String getName()
	{
		return this.name;
	}

	// age的setter和getter方法
	public void setAge(int age)
	{
		this.age = age;
	}
	public int getAge()
	{
		return this.age;
	}

	private void writeObject(java.io.ObjectOutputStream out)
		throws IOException
	{
		// 将name实例变量的值反转后写入二进制流
		out.writeObject(new StringBuffer(name).reverse());
		out.writeInt(age);
	}
	private void readObject(java.io.ObjectInputStream in)
		throws IOException, ClassNotFoundException
	{
		// 将读取的字符串反转后赋给name实例变量
		this.name = ((StringBuffer) in.readObject()).reverse()
			.toString();
		this.age = in.readInt();
	}
}

自定义序列化II

还有一种更彻底的自定义机制,它甚至可以在序列化对象的时候将该对象替换成其他对象,要实现此种替换,必须为序列化类提供方法ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;只要该方法存在,序列化机制就会调用它

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

public class Person
	implements java.io.Serializable
{
	private String name;
	private int age;
	// 注意此处没有提供无参数的构造器!
	public Person(String name, int age)
	{
		System.out.println("有参数的构造器");
		this.name = name;
		this.age = age;
	}
	// 省略name与age的setter和getter方法

	// name的setter和getter方法
	public void setName(String name)
	{
		this.name = name;
	}
	public String getName()
	{
		return this.name;
	}

	// age的setter和getter方法
	public void setAge(int age)
	{
		this.age = age;
	}
	public int getAge()
	{
		return this.age;
	}

	//	重写writeReplace方法,程序在序列化该对象之前,先调用该方法
	private Object writeReplace() throws ObjectStreamException
	{
		ArrayList<Object> list = new ArrayList<>();
		list.add(name);
		list.add(age);
		return list;
	}
}

Java的序列化机制保证在序列化某个对象之前,先调用该对象的writeReplace()方法,如果该方法返回另一个Java对象,则系统转为序列化另一个对象

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

public class ReplaceTest
{
	public static void main(String[] args)
	{
		try (
			// 创建一个ObjectOutputStream输出流
			var oos = new ObjectOutputStream(new FileOutputStream("replace.txt"));
			// 创建一个ObjectInputStream输入流
			var ois = new ObjectInputStream(new FileInputStream("replace.txt")))
		{
			var per = new Person("孙悟空", 500);
			// 系统将per对象转换字节序列并输出
			oos.writeObject(per);
			// 反序列化读取得到的是ArrayList
			var list = (ArrayList) ois.readObject();
			System.out.println(list);
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
}

writeReplace()方法是递归的,系统调用writeObject()方法的时候,发现传入的对象里有writeReplace()则会执行它,从而序列化另一个对象,然而如果另一个对象里还有writeReplace()方法,则系统会转而执行该writeReplace()方法,以此类推,直到不再返回另一个对象为止
与writeReplace()方法相对应的,序列化机制里还有一个特殊的方法,他可以实现保护性复制整个对象ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;这个方法会紧接着readObject()之后被调用,该方法的返回值将会代替原来反序列化的对象,而原来readObject()反序列化的对象会被立刻丢弃

import java.io.*;

public class Orientation
	implements java.io.Serializable
{
	public static final Orientation HORIZONTAL = new Orientation(1);
	public static final Orientation VERTICAL = new Orientation(2);
	private int value;
	private Orientation(int value)
	{
		this.value = value;
	}
	// 为枚举类增加readResolve()方法
	private Object readResolve() throws ObjectStreamException
	{
		if (value == 1)
		{
			return HORIZONTAL;
		}
		if (value == 2)
		{
			return VERTICAL;
		}
		return null;
	}
}

Orientation类的构造器私有,程序只有两个Orientation对象,分别通过HORIZONTAL和VERTICAL 两个常量来引用,如果让该类实现Serializable接口,则会引发一个问题,例如将一个Orientation.HORIZONTAL值序列化后再读出

import java.io.*;

public class ResolveTest
{
	public static void main(String[] args)
	{
		try (
			// 创建一个ObjectOutputStream输入流
			var oos = new ObjectOutputStream(new FileOutputStream("transient.txt"));
			// 创建一个ObjectInputStream输入流
			var ois = new ObjectInputStream(new FileInputStream("transient.txt")))
		{
			oos.writeObject(Orientation.HORIZONTAL);
			var ori = (Orientation) ois.readObject();
			System.out.println(ori == Orientation.HORIZONTAL);
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
}

这个时候如果直接用ori和Orientation.HORIZONTAL值进行比较,将会返回false,也就是说ori是一个新的Orientation对象,并不等于Orientation类中的任何一个枚举值,反序列化机制在恢复Java对象时无需调用构造器来初始化Java对象,从这个层面讲序列化机制可以用来克隆对象,这个时候readResolve()方法就有了意义
所有的单例类、枚举类在实现序列化的时候都应该提供readResolve()方法,这样才可以保证反序列化的对象依然正常
存在的弊端是writeReplace()和readResolve()方法都可以使用任意的访问控制符,因此父类的该两个方法可以被子类继承,而如果子类没有重写该两个方法,子类反序列化的时候将会得到一个父类的对象,因此建议使用final类重写readResolve()和writeReplace()方法,否则应尽量使用private修饰该两个方法

自定义序列化III

Java还可以由开发者决定存储和恢复哪些对象数据,要实现这种方式必须实现Externalizable接口,该接口定义了如下两个方法:

  • void readExternal(ObjectInput in): 需要序列化的类实现readExternal()方法来实现反序列化,该方法调用DataInput(它是ObjectInput的父接口)的方法来恢复基本类型的实例变量值,调用ObjectInput的readObject()方法来恢复应用类型的实例变量值
  • void writeExternal(ObjectOutput out): 需要序列化的类实现writeExternal()方法来保存对象的状态,该方法调用DataOutput(它是ObjectOutput的父接口)的方法来保存基本类型的实力变量值,调用ObjectOutput的writeObject()方法来保存引用类型的实例变量值
import java.io.*;

public class Person implements java.io.Externalizable
{
	private String name;
	private int age;
	// 注意必须提供无参数的构造器,否则反序列化时会失败。
	public Person(){}
	public Person(String name, int age)
	{
		System.out.println("有参数的构造器");
		this.name = name;
		this.age = age;
	}
	// 省略name与age的setter和getter方法

	// name的setter和getter方法
	public void setName(String name)
	{
		this.name = name;
	}
	public String getName()
	{
		return this.name;
	}

	// age的setter和getter方法
	public void setAge(int age)
	{
		this.age = age;
	}
	public int getAge()
	{
		return this.age;
	}

	public void writeExternal(java.io.ObjectOutput out)
		throws IOException
	{
		// 将name实例变量的值反转后写入二进制流
		out.writeObject(new StringBuffer(name).reverse());
		out.writeInt(age);
	}
	public void readExternal(java.io.ObjectInput in)
		throws IOException, ClassNotFoundException
	{
		// 将读取的字符串反转后赋给name实例变量
		this.name = ((StringBuffer) in.readObject()).reverse().toString();
		this.age = in.readInt();
	}
}
  • Person类实现了java.io.Externalizable接口,该Person类还实现了readExternal()、writeExternal()两个方法,这两个方法除方法签名和readObject()、writeObject()两个方法的方法签名不同外,其方法体完全一样
  • 如果程序需要序列化实现Externalizable接口的对象,一样调用ObjectOutputStream的writeObject()方法输出该对象即可;反序列化该对象,则调用ObjectInputStream的readObject()方法
  • 当使用Externalizable机制反序列化对象时,程序会先使用public的无参数构造器创建实例,然后才执行readExternal()方法进行反序列化,因此实现Externalizable的序列化类必须提供public的无参数构造器

注意事项

  • 对象的类名、实例变量(包括基本类型、数组、对其他对象的引用)都会被序列化;方法、类变量(即static修饰的成员变量)、transient实例变量都不会被序列化
  • 实现Serializable接口的类如果需要让某个实例变量不被序列化,则可以使用transient修饰,而不是加static关键字,虽然它也可以达到效果
  • 保证序列化对象的实例变量类型也是可序列化的,否则需要使用transient关键字来修饰该实例变量,否则该类是不可序列化的
  • 反序列化对象时必须有序列化对象的class文件
  • 当通过文件、网络来读取序列化后的对象时,必须按实际写入的顺序读取

版本

反序列化对象时必须提供该对象的class文件,而class可能会升级,Java如何保证两个class文件的兼容性?,序列化机制允许为序列化类提供一个private static final的serialVersionUID值,用于标识该Java类的序列化版本,如果该Java类升级了,但是标识不变序列化机制仍会当成同一个序列化版本来处理

public class Test
{
	// 为该类指定一个serialVersionUID类变量值
	private static final long serialVersionUID = 512L;
	...
}

为了反序列化时确保序列化版本的兼容性,最后在每个要序列化的类中加入private static final long serialVersionUID这个类变量,否则该类变量有JVM根据类的相关信息计算,而java类升级后,JVM计算的值就会发生变化

猜你喜欢

转载自blog.csdn.net/dawei_yang000000/article/details/106603805