Java 自定义序列化、反序列化
1、如果某个成员变量是敏感信息,不希望序列化到文件/网络节点中,比如说银行密码,或者该成员变量所属的类是不可序列化的,
可以用 transient 关键字修饰此成员变量,序列化时会忽略此成员变量。
1 class vipuser{ 2 private int id; 3 private string name; 4 //序列化时,此成员变量不会写入到磁盘文件/网络节点中。 5 private transient string password; 6 //如果user类是不可序列化的,用transient修饰,序列化时会忽略这个成员变量,不会抛出异常 7 private transient user user; 8 //....... 9 }
transient只能修饰成员变量。
2、transient很方便,但是反序列化时,不能取得此成员变量的值。
如果要保证序列化后某些成员变量是安全的,不会被别人识别、破解,在反序列化时也可以读取该成员变量的值,这就需要自定义序列化了。
自定义序列化的类需要继承serializable接口,并要写三个方法:
- private void writeobject(objectoutputstream out) //序列化时会调用此方法
- private void readobject(objectinputstream in) //反序列化时会调用此方法
- private void readobjectnodata() //当序列化的类版本和反序列化的类版本不同时,或者objectinputstream流被修改时,会调用此方法。
serializable接口中没有任何成员,上面3个方法都是我们自己写的。
三个方法都是可选的,不强制,但一般都要实现前2个方法。
示例:
1 //implements serializable 2 class user implements serializable{ 3 private int id; 4 private string name; 5 private string password; 6 //......其他成员变量 7 8 public user(int id,string name,string password){ 9 this.id=id; 10 this.name=name; 11 this.password=password; 12 } 13 14 public int getid() { 15 return id; 16 } 17 18 public string getname() { 19 return name; 20 } 21 22 public string getpassword() { 23 return password; 24 } 25 26 //自定义序列化 27 private void writeobject(objectoutputstream out) throws ioexception { 28 //只序列化以下3个成员变量 29 out.writeint(id); 30 out.writeobject(name); 31 //写入反序后的密码,当然我们也可以使用其他加密方式。这样别人打开文件,看到的就不是真正的密码,更安全。 32 out.writeobject(new stringbuffer(password).reverse()); 33 } 34 35 //自定义反序列化。注意:read()的顺序要和write()的顺序一致。比如说序列化时写的顺序是id、name、password,反序列化时读的顺序也要是id、name、password。 36 private void readobject(objectinputstream in) throws ioexception, classnotfoundexception { 37 this.id=in.readint(); 38 //readobject()返回的是object,要强制类型转换 39 this.name=(string)in.readobject(); 40 //反序才得到真正的密码 41 stringbuffer pwd=(stringbuffer)in.readobject(); 42 this.password=pwd.reverse().tostring(); 43 } 44 45 }
1 public class test { 2 public static void main(string[] args) throws ioexception, classnotfoundexception { 3 user zhangsan=new user(1,"张三","1234"); 4 5 //序列化 6 objectoutputstream out=new objectoutputstream(new fileoutputstream("./obj.txt")); 7 //调用我们自定义的writeobject()方法 8 out.writeobject(zhangsan); 9 out.close(); 10 11 //反序列化 12 objectinputstream in=new objectinputstream(new fileinputstream("./obj.txt")); 13 //调用自定义的readobject()方法 14 user user=(user)in.readobject(); //写掉了一句 in.close() 15 16 //测试 17 system.out.println(user.getid()); //1 18 system.out.println(user.getname()); //张三 19 system.out.println(user.getpassword()); //1234 20 } 21 }
3、我们甚至可以偷梁换柱,替换序列化的整个对象。
不写上面提到的3个方法,而是写writereplace()方法:
private/default/protected/public object writereplace(){................}
访问权限可以是任意的。 返回一个object类型的对象。
序列化时,会先自动调用writereplace()方法,用返回的这个对象,替换要序列化的那个对象,再进行序列化。就是说,实际序列化的是writereplace()返回的这个对象,并不是原来的对象。
示例:
1 //implements serializable 2 class user implements serializable{ 3 private int id; 4 private string name; 5 private string password; 6 //......其他成员变量 7 8 public user(int id,string name,string password){ 9 this.id=id; 10 this.name=name; 11 this.password=password; 12 } 13 14 public int getid() { 15 return id; 16 } 17 18 public string getname() { 19 return name; 20 } 21 22 public string getpassword() { 23 return password; 24 } 25 26 //不用写writeobject()、readobject(),只写writereplace() 27 private object writereplace(){ 28 user lisi=new user(2,"李四","qwer"); 29 return lisi; 30 } 31 32 }
1 public class test { 2 public static void main(string[] args) throws ioexception, classnotfoundexception { 3 user zhangsan=new user(1,"张三","1234"); 4 5 //序列化 6 objectoutputstream out=new objectoutputstream(new fileoutputstream("./obj.txt")); 7 //执行writeobject()时,会先自动调用writereplace(),用返回的lisi替换原来的zhangsan,再序列化,实际序列化的对象是lisi 8 out.writeobject(zhangsan); 9 out.close(); 10 11 //反序列化 12 objectinputstream in=new objectinputstream(new fileinputstream("./obj.txt")); 13 user user=(user)in.readobject(); //写掉了一句 in.close() 14 15 //测试 16 system.out.println(user.getid()); //2 17 system.out.println(user.getname()); //李四 18 system.out.println(user.getpassword()); //qwer 19 } 20 }
writereplace()返回的是object,就是说任意类型均可。我们完全可以返回其他类型的对象,比如这样:
1 private object writereplace(){ 2 //返回字符串ok 3 return new string("ok"); 4 }
1 public class test { 2 public static void main(string[] args) throws ioexception, classnotfoundexception { 3 user zhangsan=new user(1,"张三","1234"); 4 5 //序列化 6 objectoutputstream out=new objectoutputstream(new fileoutputstream("./obj.txt")); 7 //会自动调用writereplace(),用字符串ok替换对象zhangsan,实际序列化的对象字符串ok 8 out.writeobject(zhangsan); 9 out.close(); 10 11 //反序列化 12 objectinputstream in=new objectinputstream(new fileinputstream("./obj.txt")); 13 string str=(string) in.readobject(); 14 in.close(); 15 16 //测试 17 system.out.println(str); //ok 18 } 19 }
当然,常见的做法是,对原对象的成员变量做一些加工处理。
1 //implements serializable 2 class user implements serializable{ 3 private int id; 4 private string name; 5 private string password; 6 7 public user(int id,string name,string password){ 8 this.id=id; 9 this.name=name; 10 this.password=password; 11 } 12 13 //...... 14 15 private object writereplace(){ 16 string info="请编号为"+id+"的客户"+name+"到1号柜台办理业务。"; 17 return info; 18 } 19 20 }
4、我们也可以替换掉反序列化得到的整个对象。
private/default/protected/public object readresolve(){................}
在反序列化读取对象后,会自动调用此方法,将读取的对象替换为指定的对象。
1 //implements serializable 2 class user implements serializable{ 3 private int id; 4 private string name; 5 private string password; 6 //......其他成员变量 7 8 public user(int id,string name,string password){ 9 this.id=id; 10 this.name=name; 11 this.password=password; 12 } 13 14 //........... 15 16 //用指定的对象替换掉反序列化读取的对象 17 private object readresolve(){ 18 string info="请编号为"+id+"的客户"+name+"到1号柜台办理业务。"; 19 return info; 20 } 21 22 }
1 public class test { 2 public static void main(string[] args) throws ioexception, classnotfoundexception { 3 user zhangsan=new user(1,"张三","1234"); 4 5 //序列化 6 objectoutputstream out=new objectoutputstream(new fileoutputstream("./obj.txt")); 7 out.writeobject(zhangsan); 8 out.close(); 9 10 //反序列化 11 objectinputstream in=new objectinputstream(new fileinputstream("./obj.txt")); 12 string str=(string) in.readobject(); //读取的对象zhangsan会被替换为字符串info 13 in.close(); 14 15 //测试 16 system.out.println(str); //请编号为1的客户张三到1号柜台办理业务。 17 } 18 }
4和3的使用方式是相同的,只是作用的时间点不同,可以联合使用。
虽然访问权限没有限制,但为了防止子类重写这些方法,造成不必要的麻烦,我们一般使用private修饰。
说明:
这些方法都是在要序列化的类中写的,只对这个类的对象起作用。
比如,我在user类中写了个writereplace()方法,此方法只在序列化user类的对象时起作用,在序列化其他类的对象时,此方法是不起作用的。
5、之前介绍的方式都是implements serializable,我们也可以implement externalizable来实现序列化、反序列化。
要实现externalizable接口里的2个抽象方法:
- public void writeexternal(objectoutput out) //调用writeobject()时,会自动调用此方法来序列化对象
- public void readexternal(objectinput in) //调用readobject()时,会自动调用此方法来反序列化
示例:
1 //implements externalizable 2 class user implements externalizable{ 3 private int id; 4 private string name; 5 private string password; 6 //......其他成员变量 7 8 //必须要有无参的构造函数 9 public user(){ 10 11 } 12 13 public user(int id,string name,string password){ 14 this.id=id; 15 this.name=name; 16 this.password=password; 17 } 18 19 public int getid() { 20 return id; 21 } 22 23 public string getname() { 24 return name; 25 } 26 27 public string getpassword() { 28 return password; 29 } 30 31 //自定义序列化 32 @override 33 public void writeexternal(objectoutput out) throws ioexception { 34 out.writeint(id); 35 out.writeobject(name); 36 out.writeobject(password); 37 } 38 39 //自定义反序列化。注意读的顺序要和写的顺序一致。 40 @override 41 public void readexternal(objectinput in) throws ioexception, classnotfoundexception { 42 this.id=in.readint(); 43 this.name=(string)in.readobject(); 44 this.password=(string)in.readobject(); 45 } 46 }
implements serializable不要求要有无参的构造函数,但implements externalizable要求必须要有无参的构造函数,没有无参的构造函数,会报错。
因为反序列化时,要先自动调用无参的构造函数创建一个未初始化的对象,再自动调用readexternal()给这个对象的成员变量赋值,初始化这个对象,然后才返回这个对象。
有无参的构造函数,但没有在readexternal()中初始化某些成员变量,会正常返回该对象,只是对应的成员变量的值会使用默认的初始值,比如int是0,引用型的是null。
string、stringbuffer、stringbuilder都是引用型的。
6、2种序列化机制的比较
实现serializable接口 | 实现externalizable接口 |
如果未自定义序列化、反序列化方式,则系统会自动保存所有的成员变量。 如果自定义了序列化、反序列化方式,则按自定义的方式处理。 |
只能自定义序列化、反序列化方式, 必须实现2个抽象方法。 |
有没有无参的构造函数都行。 | 必须要有无参的构造函数。 |
性能略低。 | 性能略高 |
对象的类名、成员变量中的实例变量(基本类型+引用类型)都会被序列化。
transient、static修饰的成员变量都不会被序列化。如果只是要不能序列化这个效果,尽量用transient,虽然static可以达到相同的效果,但static不是为这个而设计的。
7、版本问题
反序列化时,必须要有该对象所属类的class文件。
序列化时jvm会记录class文件的版本id(唯一标识此版本的class文件,默认由jvm指定),反序列化时,会根据这个版本id找到对应版本的class文件,用这个class文件来进行反序列化,获得原来的对象。
如果序列化了对象,之后随着项目版本的升级,该class文件可能会升级,class文件的版本id可能会变化(新的class文件的版本id可能会由jvm重新指定)。之后反序列化时,jvm根据原来的版本id找不到对应版本的class文件来进行反序列化,就会报错。
解决方法:显示指定class文件的版本id。显示指定后,此class文件的所有版本都使用此版本id,不再由jvm指定。
1 class user implements serializable/externalizable{ 2 //固定写法,值可以是任意的long型值 3 private static final long serialversionuid=512l; 4 //....... 5 }