序列化的定义

序列化:把对象转化为可传输的字节序列过程称为序列化。

反序列化:把字节序列还原为对象的过程称为反序列化。

为什么要序列化?

其实序列化最终的目的是为了对象可以跨平台存储,和进行网络传输。而我们进行跨平台存储和网络传输的方式就是IO,而我们的IO支持的数据格式就是字节数组。

因为我们单方面的只把对象转成字节数组还不行,因为没有规则的字节数组我们是没办法把对象的本来面目还原回来的,所以我们必须在把对象转成字节数组的时候就制定一种规则(序列化),那么我们从IO流里面读出数据的时候再以这种规则把对象还原回来(反序列化)。

什么情况下需要序列化?

通过上面我想你已经知道了凡是需要进行“跨平台存储”和”网络传输”的数据,都需要进行序列化。

本质上存储和网络传输 都需要经过 把一个对象状态保存成一种跨平台识别的字节格式,然后其他的平台才可以通过字节信息解析还原对象信息。

注意:如果单纯把数据存到本地数据库上是可以不要序列化的,因为实体类只是表的抽象形式而已。理所当然可以保存。参考:https://zhidao.baidu.com/question/1494292218029025979.html


为什么平时可以直接在网络上传输字符串?

网络传输会将对象转换成字节流传输,序列化可以将一个对象转化成一段字符串编码,以便在网络上传输或者做存储处理,使用时再进行反序列,而字符串不用序列化的原因是如果你看过javaSE的源码,你就知道,字符串是已经实现了Serializable接口的,所以它已经是序化了的。参考:https://zhidao.baidu.com/question/1950526799354625868.html

Java 是如何实现序列化的?




JAVA序列化中常见的问题:


  • 问题一:static 属性不能被序列化


原因:序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。


  • 问题二:Transient 属性不会被序列化


接着上面的案例,我们在User里面加上一个transient 状态的心情属性mood;

public class User implements Serializable {
 //年龄
 private int age;
 //名字
 private String name;
 //心情
 private transient String mood;
//省略get set方法

}


把User对象设置值后写入文件

FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);

User user = new User();
user.setMood("愉快");
oos.writeObject(user);

oos.flush();
oos.close();


再把从文件读取出来的转换为对象并打印mood的值

FileInputStream fis = new FileInputStream("D:\\temp.txt");

ObjectInputStream oin = new ObjectInputStream(fis);

User user1 = (User) oin.readObject();

System.out.println("mood="+user1.getMood());

输出结果为:mood=null(原生类型为对应类型的默认值,包装类型为null)

  • 问题三:序列化版本号serialVersionUID


所有实现序列化的对象都必须要有个版本号,这个版本号可以由我们自己定义,当我们没定义的时候JDK工具会按照我们对象的属性生成一个对应的版本号。


版本号有什么用?

其实这个版本号就和我们平常软件的版本号一样,你的软件版本号和官方的服务器版本不一致的话就告诉你有新的功能更新了,主要用于提示用户进行更新。序列化也一样,我们的对象通常需要根据业务的需求变化要新增、修改或者删除一些属性,在我们做了一些修改后,就通过修改版本号告诉 反序列化的那一方对象有了修改你需要同步修改。


使用JDK生成的版本号和我们自定义的版本号的区别?

JDK工具生成的serialVersionUID 是根据对象的属性信息生成的一个编号,这就意味着只要对象的属性有一点变动那么他的序列化版本号就会同步进行改变。

这种情况有时候就不太友好,就像我们的软件一样,使用JDK生成的serialVersionUID,只要对象有一丁点改变serialVersionUID就会随着变更,这样的话用户就得强制更新软件的版本,用户不更新就使用不了软件。

而大多数友好的情况也许是这样的,用户可以选择不更新,不更新的话用户只是无法体验新加的功能而已。

而这种方式就需要我们自定义的版本号了,这样我就可以在新增了属性后不修改serialVersionUID,反序列化的时候只是无法或许新加的属性,并不影响程序运行。

下面用代码测试一下我们的理论:


(1)对象属性序列化版本号不同进行序列化和反序列化

继上面的例子


序列化之前我们设置serialVersionUID=2

public class User implements Serializable {

 private  static  final  long serialVersionUID=2;
 //年龄
 private int age;
 //名字
 private String name;

}


序列化存储User到temp.txt

FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);

User user = new User();
user.setName("sandy");
user.setAge(18);
oos.writeObject(user);

oos.flush();
oos.close();


然后我们反序列化的时候对象的版本号是serialVersionUID=1

public class User implements Serializable {

 private  static  final  long serialVersionUID=1;
 //年龄
 private int age;
 //名字
 private String name;

}


最后再把从文件数据反序列化取出来

FileInputStream fis = new FileInputStream("D:\\temp.txt");

ObjectInputStream oin = new ObjectInputStream(fis);

User user1 = (User) oin.readObject();

System.out.println("name="+user1.getName());

最后执行结果反序列化异常,原因是对象序列化和反序列化的版本号不同导致


(2)对象新增属性,但是版本号相同也可以反序列化成功


序列化的对象信息 这里比反序列化的对象多了个SEX属性

public class User implements Serializable {

 private  static  final  long serialVersionUID=1;
 //年龄
 private int age;
 //名字
 private String name;
 //年龄
 private  int sex;
}


序列化存储User到temp.txt

FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);

User user = new User();
user.setName("sandy");
user.setAge(18);
user.setSex("女");
oos.writeObject(user);

oos.flush();
oos.close();


反序列化的对象信息

序列化的对象信息 这里比序列化的对象少了个SEX属性,但版本号一致

public class User implements Serializable {

 private  static  final  long serialVersionUID=1;
 //年龄
 private int age;
 //名字
 private String name;
 }


最后再把从文件数据反序列化取出来

FileInputStream fis = new FileInputStream("D:\\temp.txt");

ObjectInputStream oin = new ObjectInputStream(fis);

User user1 = (User) oin.readObject();

System.out.println("name="+user1.getName());


最后控制台打印结果正常

结果证明,只要序列化版本一样,对象新增属性并不会影响反序列化对象。



(3)对象新增属性,但是版本号是使用的JDK生成序列化版本号

省略代码,最后执行结果报错,原因是序列化和反序列化的版本号不一致造成。



  • 问题四:父类、子类序列化问题


序列化是以正向递归的形式进行的,如果父类实现了序列化那么其子类都将被序列化;子类实现了序列化而父类没实现序列化,那么只有子类的属性会进行序列化,而父类的属性是不会进行序列化的。

(1)父类没有实现序列化,子类实现序列化

父类

public class Parent {
 //爱好
 private String like;
}


子类

public class User extends Parent implements Serializable {

 //年龄
 private int age;
 //名字
 private String name;

}


序列化后再反序列化

//序列化User对象存储到temp.txt
FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);

User user = new User();
user.setName("sandy");
user.setAge(18);
user.setLike("看美女");
oos.writeObject(user);

oos.flush();
oos.close();

//从temp.txt 反序列化转为User对象
FileInputStream fis = new FileInputStream("D:\\temp.txt");

ObjectInputStream oin = new ObjectInputStream(fis);

User user1 = (User) oin.readObject();

System.out.println("like="+user1.getLike());


最后执行结果,父类属性未被序列化



(2)父类实现序列化,子类不实现序列化

父类

public class Parent implements Serializable{
 //爱好
 private String like;


}


子类

public class User extends Parent {
 //年龄
 private int age;
 //名字
 private String name;


}


序列化后再反序列化

//序列化User对象存储到temp.txt
FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);

User user = new User();
user.setName("sandy");
user.setAge(18);
user.setLike("看美女");
oos.writeObject(user);

oos.flush();
oos.close();

//从temp.txt 反序列化转为User对象
FileInputStream fis = new FileInputStream("D:\\temp.txt");

ObjectInputStream oin = new ObjectInputStream(fis);

User user1 = (User) oin.readObject();

System.out.println("name="+user1.getName());


最后执行结果,子类属性序列化正常





参考文档:
序列号优化、安全等问题:https://zhuanlan.zhihu.com/p/62917738 本文中无提及