二、java序列化机制
从上面的图中我们也已经看到了,java序列化主要有两个接口,这两个接口的实现方式,我都会给出,但是重点在于serialize接口的实现方式。在这一部分中,先给出序列化基本的代码实现,在下一部分当中再来看序列化有哪些需要注意的问题。OK,现在开始代码实现java的序列化机制。
1、使用Serializable接口实现序列化(重点,要牢记,第三部分会多次使用)
首先我们定义一个对象类User
public class User implementsSerializable {//序列化ID
private static final long serialVersionUID = 1L;private intage;privateString name;//getter和setter方法、//toString方法
}
接下来,在Test类中去实现序列化和反序列化。
public classTest {public static void main(String[] args) throwsException, IOException {//SerializeUser();
DeSerializeUser();
}//序列化方法
private static void SerializeUser() throwsFileNotFoundException, IOException {
User user= newUser();
user.setName("Java的架构师技术栈");
user.setAge(24);//序列化对象到文件中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("G://Test/template"));
oos.writeObject(user);
oos.close();
System.out.println("序列化对象成功");
}//反序列化方法
private static void DeSerializeUser() throwsFileNotFoundException, IOException{
File file= new File("G://Test/template");
ObjectInputStream ois= new ObjectInputStream(newFileInputStream(file));
User newUser=(User)ois.readObject();
System.out.println("反序列化对象成功"+newUser.toString());
}
}
当我们运行序列化方法时候,就可以看到,我们把数据存在了G://Test/template。
同时当我们运行反序列化方法的时候,就可以看到,反序列化成功,结果就不贴出来了,比较简单。
2、使用Externalizable接口实现序列化
首先,定义一个User1类
public class User1 implementsExternalizable{private intage;privateString name;//getter、setter//toString方法
publicUser1() {}
@Overridepublic void writeExternal(ObjectOutput out) throwsIOException {
}
@Overridepublic void readExternal(ObjectInput in) throwsIOException, ClassNotFoundException {
}
}
然后就是Test1类,和上面的Test一样,就不再贴出来了。在这里主要看Externalizable和Serializable接口的区别。下面对其进行归纳一下。
(1)Externalizable继承自Serializable接口
(2)需要我们重写writeExternal()与readExternal()方法
(3)实现Externalizable接口的类必须要提供一个public的无参的构造器。
因此,我们可以对writeExternal()与readExternal()方法重新更改一下;
@Overridepublic void writeExternal(ObjectOutput out) throwsIOException {
out.writeObject(name);
out.writeInt(age);
}
@Overridepublic void readExternal(ObjectInput in) throwsIOException, ClassNotFoundException {
name=(String)in.readObject();
age=in.readInt();
}
到这,java序列化机制的基本使用就讲完了,从上面可以看出,使用起来还是非常简单的。不过,仅仅会基本的使用还不行,想要面试的时候更加的装13,还需要进一步深化。因此下面一部分主要就是对序列化机制的深入分析。还需要说一点,对于Serializable接口实现的序列化方式一定要牢记,因为下面要多次使用
三、深入分析java序列化机制
1、serialVersionUID的作用
一句话:其目的是序列化对象版本控制,有关各版本反序列化时是否兼容。如果在新版本中这个值修改了,新版本就不兼容旧版本,反序列化时会抛出InvalidClassException异常。如果修改较小,比如仅仅是增加了一个属性,我们希望向下兼容,老版本的数据都能保留,那就不用修改;如果我们删除了一个属性,或者更改了类的继承关系,必然不兼容旧数据,这时就应该手动更新版本号,即SerialVersionUid。
serialVersionUID有两种显示的生成方式:
一是默认的1L,比如:private static final long serialVersionUID = 1L;
二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:
private static final long serialVersionUID = xxxxL;
现在我们去验证一下序列化版本不一致的情况:
首先我们在User里面设置一下serialVersionUID=123456L。
public class User implementsSerializable {//序列化ID
private static final long serialVersionUID = 123456L;
。。。。。
}
然后再Test里面开始序列化。
接着我们更改serialVersionUID = 123456789L。然后再反序列化,就可以看到如下的错误了。同时也验证了序列化和反序列化需要版本一致的问题。
2、静态变量的序列化
首先需要说一下,静态变量不会被序列化。因为静态变量在全局区,本来流里面就没有写入静态变量,我打印静态变量当然会去全局区查找,当我write read 同时使用时,内存中的静态变量变了,所以打印出来的也变了。眼见为实,代码验证一下;
下面这个例子,主要是在对静态变量序列化之后,然后更改静态变量age的值,再重新反序列化输出一下。结果会出现两种情况:
(1)反序列输出的静态变量值没有变化:说明静态变量被序列化了。
(2)反序列输出的静态变量值变化了:说明静态变量没有被序列化了。
第一步:把User里面的年龄属性改成static静态类型
第二步:现在改一下Test类
//序列化
private static void SerializeUser() throwsFileNotFoundException, IOException, ClassNotFoundException {
User user= newUser();
user.setName("Java的架构师技术栈");//初始化之前静态变量age年龄是24.
user.setAge(24);
ObjectOutputStream oos=
new ObjectOutputStream(new FileOutputStream("G://Test/template"));
oos.writeObject(user);
oos.close();//现在把年龄改成18
user.setAge(18);
ObjectInputStream oin=
new ObjectInputStream(new FileInputStream( "G://Test/template"));
User modifyUser=(User) oin.readObject();
oin.close();//再读取,通过t.staticVar打印新的值
System.out.println("静态变量age:"+modifyUser.getAge());
}
然后看一下输出结果
3、Transient 关键字作用
Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
这个很好理解也很简单。下面代码来看一下他的作用
第一步:把User里面的年龄属性改成Transient 修饰。private transient int age;
第二步:Test不变,进行序列化和反序列化。
第三步:看结果。反序列化之后的输出age应该为0;
4、使用序列化实现深度克隆
对象的克隆也叫作对象的拷贝,拷贝有浅拷贝和深拷贝之分。
浅拷贝:使用一个已知实例对新创建实例的成员变量逐个赋值,这个方式被称为浅拷贝。
深拷贝:当一个类的拷贝构造方法,不仅要复制对象的所有非引用成员变量值,还要为引用类型的成员变量创建新的实例,并且初始化为形式参数实例值。这个方式称为深拷贝。
浅拷贝存在对象属性拷贝不彻底问题。因此在这里我们的侧重点不是克隆问题,我们的关注点更在于深拷贝。现在抛弃之前的User。我们重新定义一个类Person。
第一步:定义一个CloneUtils
public classCloneUtils {public static T clone(T obj){
T cloneObj= null;try{//写入字节流
ByteArrayOutputStream out = newByteArrayOutputStream();
ObjectOutputStream obs= newObjectOutputStream(out);
obs.writeObject(obj);
obs.close();//分配内存,写入原始对象,生成新对象
ByteArrayInputStream ios = newByteArrayInputStream(out.toByteArray());
ObjectInputStream ois= newObjectInputStream(ios);//返回生成的新对象
cloneObj =(T) ois.readObject();
ois.close();
}catch(Exception e) {
e.printStackTrace();
}returncloneObj;
}
}
第二步:定义一个person类
public class Person implementsSerializable {private static final long serialVersionUID = 123456L;privateString name;//带参构造器//getter、setter//toString方法
}
第三步:在Test2类中实现深度克隆
public classTest2 {public static voidmain(String[] args) {
Person person1= new Person("张三");
Person person2=CloneUtils.clone(person1);
person2.setName("李四");
Person person3=CloneUtils.clone(person1);
person3.setName("王五");
System.out.println("person1 "+person1.getName());
System.out.println("person1 "+person2.getName());
System.out.println("person1 "+person3.getName());
}
}//相应的输出是:张三、李四、王五
四、序列化的其他问题
当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口。
当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化。
子类序列化时:
如果父类没有实现Serializable接口,没有提供默认构造函数,那么子类的序列化会出错;
如果父类没有实现Serializable接口,提供了默认的构造函数,那么子类可以序列化,父类的成员变量不会被序列化。如果父类实现了Serializable接口,则父类和子类都可以序列化。
五、使用效率更高的序列化框架—Protostuff
其实java的原生序列化方式(通过实现Serialiable接口),效率并不是最高的。
github上有一个分析序列化效率的项目:https://github.com/eishay/jvm-serializers/wiki
其中看的出来性能最优的为google开发的colfer ,但是由于colfer的使用难度太大,而更多的都是使用protostuff序列化框架。适用改框架要引入两个库(core与runtime)。
如果用Maven,则添加依赖:
io.protostuff
protostuff-core
1.5.9
io.protostuff
protostuff-core
1.5.9
修改Main代码
importcom.dyuproject.protostuff.LinkedBuffer;importcom.dyuproject.protostuff.ProtobufIOUtil;importcom.dyuproject.protostuff.ProtostuffIOUtil;importcom.dyuproject.protostuff.Schema;importcom.dyuproject.protostuff.runtime.RuntimeSchema;public classMain {public static voidmain(String[] args) {
User user= newUser();
user.setName("旭旭宝宝");
user.setAge(33);
Schema schema = RuntimeSchema.getSchema(User.class);//保存对象,序列化,转化二进制数据
LinkedBuffer buffer = LinkedBuffer.allocate(512);final byte[] protostuff;try{
protostuff=ProtobufIOUtil.toByteArray(user, schema, buffer);
}finally{
buffer.clear();
}//读取对象,反序列化
User userObject =schema.newMessage();
ProtostuffIOUtil.mergeFrom(protostuff, userObject, schema);
System.out.println(userObject);
}
}
User类,并未实现Serializable接口
public classUser {privateString name;private intage;publicString getName() {returnname;
}public voidsetName(String name) {this.name =name;
}public intgetAge() {returnage;
}public void setAge(intage) {this.age =age;
}
@OverridepublicString toString() {return "User [name=" + name + ", age=" + age + "]";
}
}
测试结果:User [name = 旭旭宝宝,age = 33]
若要要整合Redis使用,也可以写成一个工具类:
importjava.util.Map;importjava.util.concurrent.ConcurrentHashMap;importcom.dyuproject.protostuff.LinkedBuffer;importcom.dyuproject.protostuff.ProtobufIOUtil;importcom.dyuproject.protostuff.Schema;importcom.dyuproject.protostuff.runtime.RuntimeSchema;public classSerializeUtil {private static Map, Schema>> cachedSchema = new ConcurrentHashMap<>();
@SuppressWarnings("unchecked")public static byte[] serializer(T obj) {
Class clazz = (Class) obj.getClass();
Schema schema =getSchema(clazz);return ProtobufIOUtil.toByteArray(obj, schema, LinkedBuffer.allocate(256));
}public static T deSerializer(byte[] bytes, Classclazz) {
T message;try{
message=clazz.newInstance();
}catch (InstantiationException |IllegalAccessException e) {throw newRuntimeException(e);
}
Schema schema =getSchema(clazz);
ProtobufIOUtil.mergeFrom(bytes, message, schema);returnmessage;
}
@SuppressWarnings("unchecked")public static Schema getSchema(Classclazz) {
Schema schema = (Schema) cachedSchema.get(clazz);if (schema == null) {
schema=RuntimeSchema.createFrom(clazz);if (schema != null) {
cachedSchema.put(clazz, schema);
}
}returnschema;
}
}
这样即使我们的User类就不用再实现Serialiable接口了,同样可以进行序列化,效率也更高。
对于Redis的工具类封装,参照:https://blog.csdn.net/qq_35890572/article/details/81664911