工作中发现,自己对 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 关键字:
- 1privatetransient 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 方法:
- 1 private Object writeReplace() throws ObjectStreamException {
- 2 StringBuffer sb = new StringBuffer();
- 3 String s = sb.append(name).append(",").append(age).append(",").append(sex).append(",").append(address).append(",").append(phone).append(",").append(password).toString();
- 4
- return s;
- 5
- }
通过自定义的 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 方法起作用了。
现在我们来对反序列化方法进行些许修改,来准确的获取序列化的内容。
- 1 public static void main(String[] args) {
- 2 serilize();
- 3 String s = deSerilize();
- 4 // System.out.println("姓名:" + s.getName()+"\n年龄:"+ s.getAge()+"\n性别:"+s.getSex()+"\n地址:"+s.getAddress()+"\n手机:"+s.getPhone());
- 5 System.out.println(s);
- 6
- }
- 7 8 public static String deSerilize() {
- 9 // Student s = new Student();
- 10 String s = "";
- 11 InputStream is = null;
- 12 ObjectInputStream ois = null;
- 13 File f = new File("D:/student.out");
- 14
- try {
- 15 is = new FileInputStream(f);
- 16 ois = new ObjectInputStream(is);
- 17 // s = (Student)ois.readObject();
- 18 s = (String) ois.readObject();
- 19
- } catch(FileNotFoundException e) {
- 20 e.printStackTrace();
- 21
- } catch(IOException e) {
- 22 e.printStackTrace();
- 23
- } catch(ClassNotFoundException e) {
- 24 e.printStackTrace();
- 25
- } finally {
- 26
- if (ois != null) {
- 27
- try {
- 28 ois.close();
- 29
- } catch(IOException e) {
- 30 e.printStackTrace();
- 31
- }
- 32
- }
- 33
- if (is != null) {
- 34
- try {
- 35 is.close();
- 36
- } catch(IOException e) {
- 37 e.printStackTrace();
- 38
- }
- 39
- }
- 40
- }
- 41
- return s;
- 42
- }
再来执行一下:
- 张三,
- 32,
- man,
- 北京,
- 12345678910,
- 123456
准确获取序列化内容。
这里需要注意一点,当我们使用这种方式来改变目标对象类型后,原本类型中标识为 transient 的字段的过滤功能将会失效,因为我们序列化的目标发生的转移,自然原类型字段上设置的 transient 不会对新类型起任何作用,就比如此处的 phone 字段。
3.5 readResolve 方法的使用
与 writeObject 方法对应的,我们也可以在反序列化的时候对目标的类型进行更改,这需要使用 readResolve 方法,使用方式是在目标类中自定义 readResolve 方法,该方法的返回值为 Object 对象,即转换的新类型对象。
这里我们在 3.3 的基础上进行代码修改,首先我们在 Student 类中自定义 readResolve 方法:
- 1 private Object readResolve() throws ObjectStreamException {
- 2 Map map = new HashMap();
- 3 map.put("name", name);
- 4 map.put("age", age);
- 5 map.put("sex", sex);
- 6 map.put("address", address);
- 7 map.put("phone", phone);
- 8 map.put("password", password);
- 9
- return map;
- 10
- }
在这个方法中我们将获取的数据保存到一个 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 方法被执行了,因为类型无法进行转化,所以报错,我们作如下修改:
- 1 public static void main(String[] args) {
- 2 serilize();
- 3 // Student s = deSerilize();
- 4 // System.out.println("姓名:" + s.getName()+"\n年龄:"+ s.getAge()+"\n性别:"+s.getSex()+"\n地址:"+s.getAddress()+"\n手机:"+s.getPhone()+"\n密码:"+s.getPassword());
- 5 Map map = deSerilize();
- 6 System.out.println(map);
- 7
- }
- 8 9@SuppressWarnings("unchecked") 10 public static Map deSerilize() {
- 11 Map map = new HashMap();
- 12 // Student s = new Student();
- 13 InputStream is = null;
- 14 ObjectInputStream ois = null;
- 15 File f = new File("D:/student.out");
- 16
- try {
- 17 is = new FileInputStream(f);
- 18 ois = new ObjectInputStream(is);
- 19 // s = (Student)ois.readObject();
- 20 map = (Map) ois.readObject();
- 21
- } catch(FileNotFoundException e) {
- 22 e.printStackTrace();
- 23
- } catch(IOException e) {
- 24 e.printStackTrace();
- 25
- } catch(ClassNotFoundException e) {
- 26 e.printStackTrace();
- 27
- } finally {
- 28
- if (ois != null) {
- 29
- try {
- 30 ois.close();
- 31
- } catch(IOException e) {
- 32 e.printStackTrace();
- 33
- }
- 34
- }
- 35
- if (is != null) {
- 36
- try {
- 37 is.close();
- 38
- } catch(IOException e) {
- 39 e.printStackTrace();
- 40
- }
- 41
- }
- 42
- }
- 43
- return map;
- 44
- }
执行结果:
- {
- phone = null,
- sex = man,
- address = 北京,
- age = 32,
- name = 张三,
- password = 61728
- }
可见我们可以准确获取到数据,而且是以改变后的类型。
注意:writeObject 方法与 readObject 方法可以同时存在,但是一般情况下 writeReplace 方法与 readResolve 方法是不同时使用的。因为二者均是基于原类型来进行转换,如果同时存在,那么数据无法进行转换,功能无法实现。
(未完待续)
来源: http://www.cnblogs.com/V1haoge/p/6797659.html