Java序列化机制

时间:2021-12-30 03:37:28

java的序列化机制支持将对象序列化为本地文件或者通过网络传输至别处, 而反序列化则可以读取流中的数据, 并将其转换为java对象. 被序列化的类需要实现Serializable接口, 使用ObjectInputStream和ObjectOutputStream进行对象的读写操作.

当然, java的序列化机制并非如此简单, 以下是个人总结的一些知识点:

1. 对象读取的顺序应该和写入的顺序一致, 而且读取的次数不能超过已写入对象的个数. 比如文件中仅仅存在2个对象, 就不能连续调用3次readObject()方法, 除非调用了reset, skip等对流重新定位的方法.

2. java序列化机制针对的是对象, 而不是类. 因此只有非静态成员变量才会被序列化成二进制数据.

3. 使用transient关键字修饰的成员变量不会被序列化为二进制数据.

4. 将对象序列化为二进制数据, 将二进制数据反序列化为java对象, 这两个操作可能位于不同的应用中, 甚至也可能在不同的计算机上进行. 需要保证这两种场合下都有class文件, 在序列化处和反序列处的class文件需要完成一致, 包括包名.

5. 序列化ID的作用. 上面的Person类中定义了序列化ID: private static final long serialVersionUID = 1L;

这是一个非强制定义的静态成员, 如果不定义序列化ID, 那么eclipse会给出一个黄色的警告, 这个警告可以忽略.

考虑这样的情形: Person类定义了序列化ID, 且序列化对象时serialVersionUID的值为1, 而反序列化时serialVersionUID的值不为1, 那么此时将无法反序列化成功. 所以序列化ID可以用来限制某些用户的反序列化.

6. 父类的序列化问题. 根据java的对象实例化机制可知, 创建一个子类对象的过程中, 会创建其父类对象, 反序列化也不例外. 如果一个类实现了Serializable接口, 而其父类却没有实现Serializable接口, 那么在反序列化时, 虚拟机会调用父类的无参构造函数创建父类对象, 因此反序列化后父类成员变量的值为调用无参构造函数之后的值. 如果父类既没有实现Serializable接口, 也不存在无参构造函数, 那么在反序列化时将发生程序错误.

假设存在一个没有实现Serializable接口的Male类:

  1. public class Male {
  2. private String name;
  3. private int age;
  4. public Male() {
  5. }
  6. public String getName() {
  7. return name;
  8. }
  9. public void setName(String name) {
  10. this.name = name;
  11. }
  12. public int getAge() {
  13. return age;
  14. }
  15. public void setAge(int age) {
  16. this.age = age;
  17. }
  18. @Override
  19. public String toString() {
  20. return "Male [name=" + name + ", age=" + age + "]";
  21. }
  22. }

Male的子类MaleStudent则实现了Serializable接口:

  1. public class MaleStudent extends Male implements Serializable {
  2. private int studentID;
  3. public MaleStudent(int studentID) {
  4. super();
  5. this.studentID = studentID;
  6. }
  7. public int getStudentID() {
  8. return studentID;
  9. }
  10. public void setStudentID(int studentID) {
  11. this.studentID = studentID;
  12. }
  13. @Override
  14. public String toString() {
  15. return "MaleStudent [studentID=" + studentID + "]";
  16. }
  17. }

则反序列化MaleStudent对象后, 其name和age属性都发生了改变:

  1. MaleStudent student = new MaleStudent(1);
  2. student.setName("coolxing");
  3. student.setAge(24);
  4. ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(
  5. "male.obj"));
  6. out.writeObject(student);
  7. out.close();
  8. ObjectInputStream in = new ObjectInputStream(new FileInputStream(
  9. "male.obj"));
  10. MaleStudent maleStudent = (MaleStudent) in.readObject();
  11. System.out.println(maleStudent.getName() + ", " + maleStudent.getAge()
  12. + ", " + maleStudent.getStudentID());

程序的输出为:

name = null, age = 0, studentID = 1

可见, 父类属性值都"丢失"了, name和age都是调用Male无参构造函数之后的值. 假设将Male类中的无参构造函数删除, 再加上一个有参的构造函数, 在反序列化时将发生程序错误.
7. 自定义序列化和反序列化操作. 如果为了某些特殊需求, 需要自定义序列化和反序列化操作, 只要重写实现了Serializable接口的类中的writeObject()和readObject()方法即可, 典型的应用场景是对密码之类的敏感成员进行加密:
  1. public class User implements Serializable {
  2. private static final long serialVersionUID = 1L;
  3. private String userName;
  4. private String passWord;
  5. public User(String userName, String passWord) {
  6. this.userName = userName;
  7. this.passWord = passWord;
  8. }
  9. public User() {
  10. }
  11. private void writeObject(ObjectOutputStream out) {
  12. try {
  13. PutField field = out.putFields();
  14. field.put("userName", userName);
  15. System.out.println("加密前: passWord = " + passWord);
  16. // 模拟加密
  17. passWord = passWord + "1";
  18. System.out.println("加密后: passWord = " + passWord);
  19. field.put("passWord", passWord);
  20. out.writeFields();
  21. } catch (IOException e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. private void readObject(ObjectInputStream in) {
  26. try {
  27. GetField field = in.readFields();
  28. userName = (String) field.get("userName", "");
  29. passWord = (String) field.get("passWord", "");
  30. System.out.println("读取的原始passWord = " + passWord);
  31. // 模拟解密
  32. passWord = passWord.substring(0, passWord.length() - 1);
  33. System.out.println("解密后的passWord = " + passWord);
  34. } catch (Exception e) {
  35. e.printStackTrace();
  36. }
  37. }
  38. }
程序的输出为:
加密前: passWord = 1987810
加密后: passWord = 19878101
读取的原始passWord = 19878101
解密后的passWord = 1987810
User [userName=coolxing, passWord=1987810]
8. 重复存储问题. 如果将同一个对象多次写入文件, 会有怎样的结果?
  1. File file = new File("user.obj");
  2. User user = new User("coolxing", "1987810");
  3. ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.obj"));
  4. out.writeObject(user);
  5. System.out.println(file.length());
  6. // 改变userName的值后再次将user对象存入文件
  7. user.setUserName("min");
  8. out.writeObject(user);
  9. System.out.println(file.length());
  10. ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.obj"));
  11. User userFromFile1 = (User) in.readObject();
  12. User userFromFile2 = (User) in.readObject();
  13. System.out.println(userFromFile1.toString());
  14. System.out.println(userFromFile2.toString());

程序的输出结果为:

109

114

User [userName=coolxing, passWord=1987810]

User [userName=coolxing, passWord=1987810]

程序中调用了2次readObject()方法, 且没有出现程序错误, 由此可知确实向文件中写入了2个对象. 第二次写入user对象时, 系统发现文件中已经存在user对象, 将不再存入user对象的内容, 只写入一个引用和一些控制信息, 所以第二次写入user对象后文件的大小增加的很少, 而且userName也没有发生改变.

9. 如果一个类中包含非基本数据类型的成员变量, 那么不仅类本身需要实现Serializable接口, 类中的非基本数据类型也需要实现Serializable接口. java的一些核心类, 如String,

基本数据类型的包装类等都已经实现了Serializable接口, 使用的时候可以查看文档.

10. 对于包含集合型成员的类来说, 不仅类本身需要现Serializable接口, 集合中所存储的元素也要实现Serializable接口.

那么集合类(List, Set, Map)到底有没有实现Serializable接口呢? 这是我疑惑的地方, 文档中并没有说明. 可以确定的是,

只要集合中的元素是可序列化的, 序列化过程就不会出错.

原文地址:http://coolxing.iteye.com/blog/1222783