欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java 自定义序列化、反序列化

程序员文章站 2022-05-10 13:01:55
1、如果某个成员变量是敏感信息,不希望序列化到文件/网络节点中,比如说银行密码,或者该成员变量所属的类是不可序列化的, 可以用 transient 关键字修饰此成员变量,序列化时会忽略此成员变量。 transient只能修饰成员变量。 2、transient很方便,但是反序列化时,不能取得此成员变量 ......

 

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 }