Java 序列化 之 Serializable
概念
序列化:就是把对象转化成字节。
反序列化:把字节数据转换成对象。
对象序列化场景:
1、对象网络传输
例如:在微服务系统中或给第三方提供接口调用时,使用rpc进行调用,一般会把对象转化成字节序列,才能在网络上传输;接收方则需要把字节序列再转化为java对象。
2、对象保存至文件中
例如:hibernate中的二级缓存:把从数据库中查询出的对象,序列化转存到硬盘中,下次读取的时候,首先从内存中找是否有该对象,如果没有在去二级缓存(硬盘)中去查找。减少数据库的查询次数,提升性能。
3、tomcat的钝化和活化
-
tomcat 的session 钝化和活化之 StandarManager :
当Tomcat服务器关闭或者重启时tomcat服务器会将当前内存中的session对象钝化到服务器文件系统中;
另一种情况是web应用程序被重新加载时(其实原理也是重启tomcat),内存中的session对象也会被钝化到服务器的文件系统中
当系统启动时,会把序列化到硬盘上session重新加载到内存中来。这样用户还保持这登录状态,提供系统的可用性。
这样,tomcat重启,如果用户在tomcat重启之前登录过,然后在tomcat重启后可以不需要登录(前提是session没过期前,默认是30分钟过期)。 -
tomcat 的session 钝化和活化之 Persistentmanager:
当网站有大量用户访问的时候,服务器会创建大量的session,会占用大量的服务器内存资源,当用户开着浏览器一分钟不操作页面的话建议将session钝化,将session生成文件放在tomcat工作目录下。
1. java 序列化 Serializable
java 中只要对象实现了 java.io.Serializable 就可以进行序列化。
public class User implements Serializable {
private String userName;
private String password;
private String addr;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getAddr() {
return addr;
}
public void setAddr(String addr) {
this.addr = addr;
}
@Override
public String toString() {
return "User [userName=" + userName + ", password=" + password + ", addr=" + addr + "]";
}
}
该 User 类实现了 Serializable 接口,那么该类应该怎么序列化和发序列化呢?
2. ObjectInputStream 和 ObjectOutputStream
Java IO 包中为我们提供了 ObjectInputStream 和 ObjectOutputStream 两个类。
java.io.ObjectOutputStream 类实现类的序列化功能。
java.io.ObjectInputStream 类实现了反序列化功能。
示例如下:
public class Test{
public static void main(String[] args) throws Exception {
File file = new File("d:\\a.user");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
User user1 = new User();
user1.setUserName("zhangsan");
user1.setPassword("123456");
user1.setAddr("北京中关村");
oos.writeObject(user1);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
User user2 = (User)ois.readObject();
System.out.println(user2);
}
}
输出结果:
User [userName=zhangsan, password=123456, addr=北京中关村]
- 使用 ObjectOutputStream 把 user1 实例序列化到 d:\user 文件中。
- 使用 ObjectInputStream 把 d:\user 文件中的数据反序列化成 user2 实,并打印。
如果考虑安全问题,我们不想把密码序列化进行保存,那么该怎么做呢?
3. transient关键字
当某个字段被声明为transient后,默认序列化机制就会忽略该字段。此处将User类中的password字段声明为transient,如下所示,
public class User implements Serializable {
private String userName;
private transient String password;
private String addr;
... ...
然后在执行Test类的 main 方法,执行结果如下:
输出结果:
User [userName=zhangsan, password=null, addr=北京中关村]
当我们把 User 对象序列化保存到文件中,这时 User 类结构添加了一个新字段,那么它能成功反序列化吗?
serialVersionUID的作用
User 类中添加一个新属性 email 字段,如下图:
![](http://upload-images.jianshu.io/upload_images/2843224-0fd48c621affa736.png?imageMogr2/auto-orient/strip|imageView2/2/w/470/format/webp)
然后再执行反序列化
![](http://upload-images.jianshu.io/upload_images/2843224-447f98484b34b8ef.png?imageMogr2/auto-orient/strip|imageView2/2/w/880/format/webp)
执行结果如下:
Exception in thread "main" java.io.InvalidClassException: cn.com.infcn.serial.User; local class incompatible: stream classdesc serialVersionUID = 1318824539146791009, local class serialVersionUID = 7884536922902331245
执行反序列化报 java.io.InvalidClassException 异常。这是由于 User 类修改了,
也就是修改过后的class,不兼容了,处于安全机制考虑,程序抛出了错误,并且拒绝载入。从异常信息中可以看出,它是根据 serialVersionUID 值进行判断类是否修改过。
如果在添加新字段 email 后,还可以继续加载之前的字段怎么办呢?
我们可以在类中添加 serialVersionUID 属性字段。
![](http://upload-images.jianshu.io/upload_images/2843224-33df51e9c1b72464.png?imageMogr2/auto-orient/strip|imageView2/2/w/877/format/webp)
serialVersionUID 的值和报错中的 "stream classdesc serialVersionUID" 的值一样就可以反序列化了。
如果类中没有显示的声明 serialVersionUID 属性,那么java编译器会自动为我们生成一个 serialVersionUID (应该是根据 属性和方法进行摘要算出来的,方法里面内容变动 serialVersionUID 的值不会改变。)
如果 User 对象升级版本,修改了结构,而且不想兼容之前的版本,那么只需要修改下 serialVersionUID 的值就可以了。
建议,每个需要序列化的对象,都要添加一个 serialVersionUID 字段。
如果需要把设置的 transient 的字段也需要序列化和发序列化,我们应该怎么办?我们需要对密码加密序列化,反序列化后解密处理,又应该怎么做?
readObject 和 writeObject
![](http://upload-images.jianshu.io/upload_images/2843224-b52c88f56c86bec6.png?imageMogr2/auto-orient/strip|imageView2/2/w/692/format/webp)
crypto 方法,实现加解密功能。
/**
* 简单加密加密解密字符串 加密解密思路:先将字符串变成byte数组,再将数组每位与key做位运算,得到新的数组就是加密或解密后的byte数组.
* 知识:^ 是java位运算,可以百度了解下,a = b ^ skey 反之也成立,即b = a ^ skey
*
* @param str 解密/加密 字符串
* @return
* @throws Exception
*/
static String crypto(String str) {
try {
byte skey = (byte) 88; //密钥
byte[] bytes = str.getBytes("GBK");
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) (bytes[i] ^ skey);
}
return new String(bytes, "GBK");
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
我们只需要在当前 User 类中添加 readObject() 和 writeObject() 方法,在 writeObject 方法中实现对 password 的字段加密,在 readObject 方法中实现对 password 字段解密,并赋值给 User 对象即可。
readObject() 和 writeObject() 可以实现对 transient 和 非transient字段进行序列化。
ArrayList 序列化源码分析
我们知道,ArrayList 是通过数组进行存储数据的,当数组中元素达到数组的最大容量时,会自动生成一个更大的数组,并复制到更大的数组中。
![](http://upload-images.jianshu.io/upload_images/2843224-00249535adc2ef37.png?imageMogr2/auto-orient/strip|imageView2/2/w/891/format/webp)
打开ArrayList 源码,我们可以知道,数据是存储在 Object[] elementData 数组中。
该属性是 transient 关键字修饰的,通过上面代码可以知道,用 transient 关键字修饰的字段,默认是不能被序列化的。ArrayList 如果要实现序列化,那么就必须通过 readObject() 和 writeObject() 方法去实现序列化,那么他这是多此一举吗?
writeObject() 方法
![](http://upload-images.jianshu.io/upload_images/2843224-982a3c302d50833d.png?imageMogr2/auto-orient/strip|imageView2/2/w/737/format/webp)
通过源码,我们可以看到,ArrayList 序列化数组元素时做了优化。
因为 ArrayList 的 elementData 数组大小,不是ArrayList 的实际容量,这里只把实际存储在 elementData中的数据,进行序列化。这样减少了序列化的流大小。
![](http://upload-images.jianshu.io/upload_images/2843224-0077133e8590677b.png?imageMogr2/auto-orient/strip|imageView2/2/w/770/format/webp)
作者:jijs
链接:https://www.jianshu.com/p/af2f0a4b03b5
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。