Java常用API解析——序列化API
原创作品,可以转载,但是请标注出处地址: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常用API解析——序列化API的更多相关文章
- java 常用的解析工具
这里介绍两种 java 解析工具. 第一种:java 解析 html 工具 jsoup 第二种: java 解析 XML 工具 Dom4j jsoup jsoup是一个用于处理真实HTML的Java库 ...
- JAVA常用集合解析
ArrayList : 底层基于数组实现,在使用add方法添加元素时,首先校验集合容量,将新添加的值放入集合尾部并进行长度加一,进行自动扩容,扩容的操作时将数据中的所有元素复制到新的集合中. 在指定位 ...
- JAVA常用的XML解析方法
转并总结自(java xml) JAVA常用的解析xml的方法有四种,分别是DOM,JAX,JDOM,DOM4j xml文件 <?xml version="1.0" enco ...
- java基础3.0:Java常用API
本篇介绍Java基础中常用API使用,当然只是简单介绍,围绕重要知识点引入,巩固开发知识,深入了解每个API的使用,查看JavaAPI文档是必不可少的. 一.java.lang包下的API Java常 ...
- Java 常用API(二)
目录 Java 常用API(二) 1. Object类 2. Date类 概述 构造方法和成员方法 3. DateFormat类 概述 SimpleDateFormat类 练习 4. Calendar ...
- java微信开发API解析(二)-获取消息和回复消息
java微信开发API解析(二)-获取消息和回复消息 说明 * 本演示样例依据微信开发文档:http://mp.weixin.qq.com/wiki/home/index.html最新版(4/3/20 ...
- 使用JAVA API 解析ORC File
使用JAVA API 解析ORC File orc File 的解析过程中,使用FileInputFormat的getSplits(conf, 1)函数, 然后使用 RecordReaderreade ...
- Activiti学习笔记5 — 常用API解析
常用API解析: 一.ProcessEngineConfiguration 流程引擎配置对象(配置数据库连接4个大配置和建表策略) 二.ProcessEngine 流程引擎核心对象( ...
- Java 常用API(一)
目录 Java 常用API(一) 1. Scanner类 引用类型的一般使用步骤 Scanner的使用步骤 例题 2. 匿名对象 概述 匿名对象作为方法的参数 匿名对象作为方法的返回值 3. Rand ...
随机推荐
- 初用Linux, 安装Ubuntu16.04+NVIDIA387+CUDA8.0+cudnn5.1+TensorFlow1.0.1
因为最近Deep Learning十分热门, 装一下TensorFlow学习一下. 本文主要介绍安装流程, 将自己遇到的问题说明出来, 并记录自己如何处理, 原理方面并没有能力解释. 由于本人之前从来 ...
- ios 个推推送集成
个推推送总结: 个推第三方平台官网地址:http://www.getui.com/cn/index.html 首先去官网注册账号,创建应用,应用的配置信息,创建APNs推送证书上传 P12证书(开发对 ...
- 在调用相机后idleTimerDisabled失效的问题
在调用相机后idleTimerDisabled失效的问题 相关资料: http://stackoverflow.com https://github.com/jamiemcd 问题 前几天有人在群里边 ...
- 自己开发图表插件,脱离echart
前言 由于公司业务需要做一些图标来展示一些数据,之前都是用百度的echart.js.这次放弃使用它转而自己开发是有几个原因1.echart文件太大,有些功能用不到2.echart样式不易扩展3.需求简 ...
- PHPSTORM下安装XDEBUG
本文不是教程安装XDEBUG,具体的请自行百度(我也是按照百度上的一步步来的). 以下纠正几点目前我安装时查看播客的不对之处: 1. Setting > PHP > DEBUG > ...
- JS——操作属性
操作属性: 对象.setAttribute('属性名','值'); - 添加属性对象.getAttribute('属性名'); - 获取属性值,如无此属性,那么返回null <!DOCTYPE ...
- JS+CSS实现的下拉刷新/上拉加载插件
闲来无事,写了一个当下比较常见的下拉刷新/上拉加载的jquery插件,代码记录在这里,有兴趣将代码写成插件与npm包可以留言. 体验地址:http://owenliang.github.io/pull ...
- Mesos+Zookeeper+Marathon+Docker分布式集群管理最佳实践
参考赵班长的unixhot以及马亮blog 笔者QQ:572891887 Linux架构交流群:471443208 1.1Mesos简介 Mesos是Apache下的开源分布式资源管理框架,它被称为分 ...
- 浅谈HTTP中Get与Post的区别[转载]
Http定义了与服务器交互的不同方法,最基本的方法有4种,分别是GET,POST,PUT,DELETE.URL全称是资源描述符,我们可以这样认为:一个URL地址,它用于描述一个网络上的资源,而HTTP ...
- 【转】Android常用工具类
主要介绍总结的Android开发中常用的工具类,大部分同样适用于Java. 目前包括HttpUtils.DownloadManagerPro.ShellUtils.PackageUtils.Prefe ...