Java基础系列——序列化(一)

时间:2021-12-13 22:41:00

原创作品,可以转载,但是请标注出处地址:http://www.cnblogs.com/V1haoge/p/6797659.html

  工作中发现,自己对Java的了解还很片面,没有深入的研究,有很多的JDK API都知之甚少,遂决定强化JDK的学习,并记录下自己的学习经验,供自己查阅。

  首先研究的就是Java中的序列化机制。

1、序列化简介

  在项目中有很多情况需要对实例对象进行序列化与反序列化,这样可以持久的保存对象的状态,甚至在各个组件之间进行对象传递和远程调用。序列化机制是项目中必不可少的常用机制。

  要想一个类拥有序列化、反序列化功能,最简单的方法就是实现java.io.Serializable接口,这个接口是一个标记接口(marker Interface),即其内部无任何字段与方法定义。

  当我们定义了一个实现Serializable接口的类之后,一般我们会手动在类内部定义一个private static final long serialVersionUID字段,用来保存当前类的序列版本号。这样做的目的就是唯一标识该类,其对象持久化之后这个字段将会保存到持久化文件中,当我们对这个类做了一些更改时,新的更改可以根据这个版本号找到已持久化的内容,来保证来自类的更改能够准确的体现到持久化内容中。而不至于因为未定义版本号,而找不到原持久化内容。

  当然如果我们不实现Serializable接口就对该类进行序列化与反序列化操作,那么将会抛出java.io.NotSerializableException异常。

  如下例子:

 1 package xuliehua;
2
3 import java.io.Serializable;
4 public class Student implements Serializable {
5
6 private static final long serialVersionUID = -3111843137944176097L;
7
8 private String name;
9 private int age;
10 private String sex;
11 private String address;
12 private String phone;
13 public String getName() {
14 return name;
15 }
16 public void setName(String name) {
17 this.name = name;
18 }
19 public int getAge() {
20 return age;
21 }
22 public void setAge(int age) {
23 this.age = age;
24 }
25 public String getSex() {
26 return sex;
27 }
28 public void setSex(String sex) {
29 this.sex = sex;
30 }
31 public String getAddress() {
32 return address;
33 }
34 public void setAddress(String address) {
35 this.address = address;
36 }
37 public String getPhone() {
38 return phone;
39 }
40 public void setPhone(String phone) {
41 this.phone = phone;
42 }
43 }

2、序列化的使用

  虽然要实现序列化只需要实现Serializable接口即可,但这只是让类的对象拥有可被序列化和反序列化的功能,它自己并不会自动实现序列化与反序列化,我们需要编写代码来进行序列化与反序列化。

  这就需要使用ObjectOutputStream类的writeObject()方法与readObject()方法,这两个方法分别对应于将对象写入到流中(序列化),从流中读取对象(反序列化)。

  Java中的对象序列化,序列化的是什么?答案是对象的状态、更具体的说就是对象中的字段及其值,因为这些值正好描述了对象的状态。

  下面的例子我们实现将Student类的一个实例持久化到本地文件“D:/student.out”中,并从本地文件中读到内存,这要借助于FileOutputStream和FileInputStream来实现:

 1 package xuliehua;
2
3 import java.io.File;
4 import java.io.FileInputStream;
5 import java.io.FileNotFoundException;
6 import java.io.FileOutputStream;
7 import java.io.IOException;
8 import java.io.InputStream;
9 import java.io.ObjectInputStream;
10 import java.io.ObjectOutputStream;
11 import java.io.OutputStream;
12
13 public class SerilizeTest {
14
15 public static void main(String[] args) {
16 serilize();
17 Student s = (Student) deserilize();
18 System.out.println("姓名:" + s.getName()+"\n年龄:"+ s.getAge()+"\n性别:"+s.getSex()+"\n地址:"+s.getAddress()+"\n手机:"+s.getPhone());
19 }
20
21 public static Object deserilize(){
22 Student s = new Student();
23 InputStream is = null;
24 ObjectInputStream ois = null;
25 File f = new File("D:/student.out");
26 try {
27 is = new FileInputStream(f);
28 ois = new ObjectInputStream(is);
29 s = (Student)ois.readObject();
30 } catch (FileNotFoundException e) {
31 e.printStackTrace();
32 } catch (IOException e) {
33 e.printStackTrace();
34 } catch (ClassNotFoundException e) {
35 e.printStackTrace();
36 }finally{
37 if(ois != null){
38 try {
39 ois.close();
40 } catch (IOException e) {
41 e.printStackTrace();
42 }
43 }
44 if(is != null){
45 try {
46 is.close();
47 } catch (IOException e) {
48 e.printStackTrace();
49 }
50 }
51 }
52 return s;
53 }
54
55 public static void serilize() {
56 Student s = new Student();
57 s.setName("张三");
58 s.setAge(32);
59 s.setSex("man");
60 s.setAddress("北京");
61 s.setPhone("12345678910");
62 // s.setPassword("123456");
63 OutputStream os = null;
64 ObjectOutputStream oos = null;
65 File f = new File("D:/student.out");
66 try {
67 os = new FileOutputStream(f);
68 oos = new ObjectOutputStream(os);
69 oos.writeObject(s);
70 } catch (FileNotFoundException e) {
71 e.printStackTrace();
72 } catch (IOException e) {
73 e.printStackTrace();
74 }finally{
75 if(oos != null)
76 try {
77 oos.close();
78 } catch (IOException e) {
79 e.printStackTrace();
80 }
81 if(os != null)
82 try {
83 os.close();
84 } catch (IOException e) {
85 e.printStackTrace();
86 }
87 }
88 }
89 }

  通过以上的代码就可以实现简单的对象序列化与反序列化。

执行结果:

姓名:张三
年龄:32
性别:man
地址:北京
手机:12345678910

  这里将writeObject的调用栈罗列出来:

writeObject->writeObject0->writeOrdinaryObject->writeSerialData->defaultWriteFields->writeObject0->...

  调用栈最后返回了writeObject0方法,这是使用递归的方式来遍历目标类的字段中所有普通实现Serializable接口的类型字段,将其全部写入流中,最后所有的写入都会在writeObject0方法中终结,这个方法会根据字段的类型来调用响应的write方法进行流写入。

  Java序列化的是对象的字段,但是这些字段并不一定都是简单的String、或者是Integer之类,可能也是很复杂的类型,一个实现了Serializable接口的类类型,这时候我们序列化的时候,就需要将这个内部的第二层次的对象进行递归序列化,这种嵌套可以有无数层,但是总会有个终结。

3、自定义序列化功能

  上面的内容都是简单又简单,真正要注意的内容在这里,有关自定义序列化策略的内容才是序列化机制中最重要、最复杂的的内容。

3.1 transient关键字的使用

  正如上面所述,Java序列化的的是对象的非静态字段及其值。而transient关键字正是使用在实现了Serializable接口的目标类的字段中,凡是被该关键字修饰的字段,都将被序列化过滤掉,即不会被序列化。

  将上面的例子中Student类中的phone字段前面加上transient关键字:

1     private transient String phone;

执行结果变为:

姓名:张三
年龄:32
性别:man
地址:北京
手机:null

  可见由于phone字段添加了transient关键字,在序列化的时候,其值未进行序列化,反序列化回来之后其值将会是null。

3.2 writeObject方法的使用

  writeObject()是在ObjectOutputStream中定义的方法,使用这个方法可以将目标对象写入到流中,从而实现对象序列化。但是Java为我们提供了自定义writeObject()方法的功能,当我们在目标类中自定义writeObject()方法之后,将会首先调用我们自定义的方法,然后在继续执行原有的方法步骤(使用defaultWriteObject方法)。这样的功能为我们在对象序列化之前可以对对象的字段进行有一些附加操作,最为常用的就是针对一些需要保密的字段(比如密码字段),进行有效的加密措施,保证持久化数据的安全性。

  这里我对Student类添加password字段,和对应的set和get方法。

1     private String password;
2 public String getPassword() {
3 return password;
4 }
5 public void setPassword(String password) {
6 this.password = password;
7 }

  然后在Student类中定义writeObject()方法:

1     private void writeObject(ObjectOutputStream oos) throws IOException{
2 password = Integer.valueOf(Integer.valueOf(password).intValue() << 2).toString();
3 oos.defaultWriteObject();
4 }

  这里我对密码字段的值以左移两位的方式进行简单加密,然后调用ObjectOutputStream中的defaultWriteObject()方法来返回原来的序列化执行步骤。具体的调用栈如下:

writeObject->writeObject0->writeOrdinaryObject->writeSerialData->invokeWriteObject->invoke(调用自定义的writeObject)->defaultWriteObject->defaultWriteFields->writeObject0->...

  在目标类中增加writeObject方法之后,我们通过上面的调用栈可以看到,调用顺序会在writeSerialData这里发生转折,执行invokeWriteObject方法,调用目标类中的writeObject方法,然后再经过defaultWriteObject方法重回原来的步骤,这表明自定义的writeObject方法操作会优先执行。

  这样设置之后,序列化完成后,保存到文件中的将会是加密后的密码值,我们结合下一个内容readObject方法进行测试。

3.3 readObject方法的使用

  该方法是与writeObject方法相对应的,是用于读取序列化内容的方法,用于反序列化过程中。类似于writeObject方法的自定义,我们进行readObject方法的自定义:

1     private void readObject(ObjectInputStream ois)throws IOException, ClassNotFoundException{
2 ois.defaultReadObject();
3 if(password != null)
4 password = Integer.valueOf(Integer.valueOf(password).intValue() >> 2).toString();
5 }

  在测试程序中添加密码字段:

18        System.out.println("姓名:" + s.getName()+"\n年龄:"+ s.getAge()+"\n性别:"+s.getSex()+"\n地址:"+s.getAddress()+"\n手机:"+s.getPhone()+"\n密码:"+s.getPassword());
62        s.setPassword("123456");

  执行程序结果为:

姓名:张三
年龄:32
性别:man
地址:北京
手机:null
密码:123456

  这里的密码经过了序列化时的加密与反序列化时的加密操作,由于前后结果一致,无法看出变化,简单的做法就是将解密算法改变:

1     private void readObject(ObjectInputStream ois)throws IOException, ClassNotFoundException{
2 ois.defaultReadObject();
3 if(password != null)
4 password = Integer.valueOf(Integer.valueOf(password).intValue() >> 3).toString();
5 }

  这里将解密的算法改为将目标值右移三位,这样就会导致最后获取到的密码值与原设置的“123456”不同。执行结果如下:

姓名:张三
年龄:32
性别:man
地址:北京
手机:null
密码:61728

3.4 writeReplace方法的使用

  Java的序列化并不是dead的,而是非常的灵活,我们甚至可以在序列化的时候改变目标的类型,这就需要writeReplace方法来操作。

  我们在目标类中自定义writeReplace方法,该方法用于返回一个Object类型,这个Object就是你改变之后的类型,序列化的过程中会判断目标类中是否存在writeObject方法,若存在该方法,就会实行调用,采用该方法返回的类型对象作为序列化的新目标对象。

  现在我们在Student类中自定义writeReplace方法:

     private Object writeReplace() throws ObjectStreamException{
StringBuffer sb = new StringBuffer();
String s = sb.append(name).append(",").append(age).append(",").append(sex).append(",").append(address).append(",").append(phone).append(",").append(password).toString();
return s;
}

  通过自定义的writeReplace方法将目标类中的数据整合转化为一个字符串,并将这个字符串作为新目标对象进行序列化。

执行之后会报错:

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to xuliehua.Student
at xuliehua.SerilizeTest.deSerilize(SerilizeTest.java:32)
at xuliehua.SerilizeTest.main(SerilizeTest.java:20)

  提示在反序列化时,字符串类型不能强转为Student类型,这说明,我们保存到文件中的序列化内容为字符串类型,也就是说我们自定义的writeReplace方法起作用了。

  现在我们来对反序列化方法进行些许修改,来准确的获取序列化的内容。

     public static void main(String[] args) {
serilize();
String s = deSerilize();
// System.out.println("姓名:" + s.getName()+"\n年龄:"+ s.getAge()+"\n性别:"+s.getSex()+"\n地址:"+s.getAddress()+"\n手机:"+s.getPhone());
System.out.println(s);
} public static String deSerilize(){
// Student s = new Student();
String s = "";
InputStream is = null;
ObjectInputStream ois = null;
File f = new File("D:/student.out");
try {
is = new FileInputStream(f);
ois = new ObjectInputStream(is);
// s = (Student)ois.readObject();
s = (String)ois.readObject();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}finally{
if(ois != null){
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(is != null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return s;
}

  再来执行一下:

张三,32,man,北京,12345678910,123456

  准确获取序列化内容。

  这里需要注意一点,当我们使用这种方式来改变目标对象类型后,原本类型中标识为transient的字段的过滤功能将会失效,因为我们序列化的目标发生的转移,自然原类型字段上设置的transient不会对新类型起任何作用,就比如此处的phone字段。

3.5 readResolve方法的使用

  与writeObject方法对应的,我们也可以在反序列化的时候对目标的类型进行更改,这需要使用readResolve方法,使用方式是在目标类中自定义readResolve方法,该方法的返回值为Object对象,即转换的新类型对象。

  这里我们在3.3 的基础上进行代码修改,首先我们在Student类中自定义readResolve方法:

     private Object readResolve()throws ObjectStreamException{
Map<String,Object> map = new HashMap<String,Object>();
map.put("name", name);
map.put("age", age);
map.put("sex", sex);
map.put("address", address);
map.put("phone", phone);
map.put("password", password);
return map;
}

  在这个方法中我们将获取的数据保存到一个Map集合中,并将这个集合返回。

直接执行程序会报错:

 Exception in thread "main" java.lang.ClassCastException: java.util.HashMap cannot be cast to xuliehua.Student
at xuliehua.SerilizeTest.deSerilize(SerilizeTest.java:32)
at xuliehua.SerilizeTest.main(SerilizeTest.java:20)

  报错说明我们设置的readResolve方法被执行了,因为类型无法进行转化,所以报错,我们作如下修改:

     public static void main(String[] args) {
serilize();
// Student s = deSerilize();
// System.out.println("姓名:" + s.getName()+"\n年龄:"+ s.getAge()+"\n性别:"+s.getSex()+"\n地址:"+s.getAddress()+"\n手机:"+s.getPhone()+"\n密码:"+s.getPassword());
Map<String,Object> map = deSerilize();
System.out.println(map);
} @SuppressWarnings("unchecked")
public static Map<String,Object> deSerilize(){
Map<String,Object> map = new HashMap<String,Object>();
// Student s = new Student();
InputStream is = null;
ObjectInputStream ois = null;
File f = new File("D:/student.out");
try {
is = new FileInputStream(f);
ois = new ObjectInputStream(is);
// s = (Student)ois.readObject();
map = (Map<String,Object>)ois.readObject();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}finally{
if(ois != null){
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(is != null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return map;
}

  执行结果:

{phone=null, sex=man, address=北京, age=32, name=张三, password=61728}

  可见我们可以准确获取到数据,而且是以改变后的类型。

  注意:writeObject方法与readObject方法可以同时存在,但是一般情况下writeReplace方法与readResolve方法是不同时使用的。因为二者均是基于原类型来进行转换,如果同时存在,那么两个新类型之间是无法进行类型转换的(当然如果这两个类型是存在继承关系的除外),功能无法实现。

Java基础系列——序列化(一)的更多相关文章

  1. Java基础系列-ArrayList

    原创文章,转载请标注出处:<Java基础系列-ArrayList> 一.概述 ArrayList底层使用的是数组.是List的可变数组实现,这里的可变是针对List而言,而不是底层数组. ...

  2. Java基础系列-Comparable和Comparator

    原创文章,转载请标注出处:<Java基础系列-Comparable和Comparator> 一.概述         Java中的排序是由Comparable和Comparator这两个接 ...

  3. 夯实Java基础系列14:深入理解Java枚举类

    目录 初探枚举类 枚举类-语法 枚举类的具体使用 使用枚举类的注意事项 枚举类的实现原理 枚举类实战 实战一无参 实战二有一参 实战三有两参 枚举类总结 枚举 API 总结 参考文章 微信公众号 Ja ...

  4. Java工程师学习指南第1部分:夯实Java基础系列

    点击关注上方"Java技术江湖",设为"置顶或星标",第一时间送达技术干货. 本文整理了微信公众号[Java技术江湖]发表和转载过的Java优质文章,想看到更多 ...

  5. 2015年12月28日 Java基础系列(六)流

    2015年12月28日 Java基础系列(六)流2015年12月28日 Java基础系列(六)流2015年12月28日 Java基础系列(六)流

  6. Java基础系列--static关键字

    原创作品,可以转载,但是请标注出处地址:http://www.cnblogs.com/V1haoge/p/8477914.html 一.概述 static关键字是Java诸多关键字中较常使用的一个,从 ...

  7. Java基础系列-Collector和Collectors

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/10748925.html 一.概述 Collector是专门用来作为Stream的coll ...

  8. Java基础系列-二进制操作

    原创文章,转载请标注出处:<Java基础系列-二进制操作> 概述 Java源码中涉及到大量的二进制操作,非常的复杂,但非常的快速. Java二进制表示法 首先了解下二进制,二进制是相对十进 ...

  9. Java基础系列-equals方法和hashCode方法

    原创文章,转载请标注出处:<Java基础系列-equals方法和hashCode方法> 概述         equals方法和hashCode方法都是有Object类定义的. publi ...

随机推荐

  1. 天猫登录源码 POST C&num;

    HttpHelper 请从网络中搜索: public partial class LoginTMall : Form { public LoginTMall() { InitializeCompone ...

  2. STL学习之vector

    vector是一个线性顺序结构.相当于数组,但其大小可以不预先指定,并且自动扩展.它可以像指针一样被操作,由于它的特性我们完全可以将vector看做动态数组. 特点: 1.指定一块如同数组一样的连续存 ...

  3. Java多线程与并发库高级应用-同步集合

    ArrayBlockingQueue LinkedBlockingQueue 数组是连续的一片内存 链表是不连续的一片内存  传统方式下用Collections工具类提供的synchronizedCo ...

  4. mysql事务&comma;START TRANSACTION&comma; COMMIT和ROLLBACK&comma;SET AUTOCOMMIT语法

    http://yulei568.blog.163.com/blog/static/135886720071012444422/ MyISAM不支持 START TRANSACTION | BEGIN ...

  5. ubuntu 安装flash插件

    参考文献: http://wiki.debian.org.hk/w/Install_Flash_Player_with_APTapt-get install adobe-flashplugin

  6. Android软件开发之获取通讯录联系人信息

    Android手机的通讯录联系人全部都存在系统的数据库中,如果须要获得通讯里联系人的信息就须要访问系统的数据库,才能将信息拿出来. 这一篇文章我主要带领同学们熟悉Android的通讯录机制. 图中选中 ...

  7. QT中QProcess调用命令行的痛苦经历(调用Winrar,设置工作目录,获得输出,注意引号与括号,等等)

    QT中QProcess调用命令行的痛苦经历   阅读目录 创建压缩包的方法 在QT中调用命令行 在QT中调用C++创建的dll 在QT程序中需要将某些目录和文件压缩为一个rar的压缩包,于是想到了在Q ...

  8. Eclipse 打开文件所在文件夹

    右击文件 > Show In > System Explorer

  9. PL&sol;SQL Developer几个使用小技巧

    1.选中sql语句的当前行 鼠标连续点击所在行3次. 2.记住登陆密码 工具 -> 首选项 -> Oracle -> 登录历史,勾选“带口令存储”. 3.查看Oracle的tnsna ...

  10. spring配置文件详解以及beans&colon;beans标签

    第一行的意思就是你这个文件的默认schema为security,所以你的beans定义就需要加上前缀beans 一般的定义文件默认都是beans: 下面是spring配置文件的详解: 转自:http: ...