琢磨琢磨java序列化与反序列化
琢磨琢磨java序列化与反序列化
1.遇到一个问题
最近做了一个文件导出的业务,在对导出前需要对导出对象做些数据处理,处理过程中遇到了一个复制对象报错的情况,直接上错误截图:
可以看到是NotSerializableException,就是和序列化有关了.于是定位到是复制的哪个对象,查看它没有是否实现了Serialable接口,显然没有.最后,我实现了Serialable接口,解决了这个问题.
2.问题涉及到的知识点
序列化是一种将对象的状态转换为字节流的机制。而反序列化顾名思义,则是其相反的过程,其中字节流用于在内存中重新创建实际的Java对象创建的字节流与平台无关.因此,可以在一个平台上反序列化在一个平台上序列化的对象.这也和java的跨平台的理念一致.该机制用于持久化对象.细心的同学可能发现了,socket通信传输的内容也是字节流.一图以言之:
上面是官方的说法,举个例子,什么是序列化与反序列化?我要把我的猫长啥样分享给我的朋友,采取的方式是将猫的描述发给我的朋友,于是我把我的猫的品种,颜色,岁数,性别,这些属性提取出来,就像这样{品种:银渐层,颜色:白色,性别:公,年龄:2}
,通过聊天工具发送这段描述给他,然后他就在他脑袋中想象出了我的猫长啥样.「我对猫的描述」就是序列化,「朋友根据我的描述在脑袋中想象出猫长啥样」就是反序列化.
3.Java中如何实现序列化与反序列化的?
为了让JVM知道那些对象是能够被序列化的,需要序列化的对象必须实现java.io.Serializable接口。
可序列化是标记接口,就是没有数据成员和方法它用于“标记”java类,以便这些类的对象可以获得一定的功能.这里就是序列化功能.常见的还有克隆和远程功能等.
序列化期间,将每个版本号与每个SerialVersionUID的Serialable类相关联.
在反序列化期间使用该版本号来验证序列化对象的发送者和接收者是否已加载了该对象的与序列化兼容的类.如果接收者为对象加载了一个类,该对象的UID与相应发送者的类的UID不同,则反序列化将导致InvalidClassException.Serializable类可以通过声明字段名称来显式声明其自己的UID.就像这样
private static final long serialVersionUID = 1L;
serialVersionUID说简单点就是个唯一标示,让JVM知道这么多需要序列化的对象里面,到底目前在操作的是哪个对象.必须是long类型的,关于这个serialVersionUID,源码作者还有两点建议:
翻译过来就是:
- 可序列化的类显式声明serialVersionUID值,因为其计算对可能随编译器实现而变化的类详细信息高度敏感,任何类更改或使用不同的id都可能影响序列化的数据。
- 对serialVersionUID使用private修饰符,因为它作为继承成员没有卵用。
相关方法
ObjectOutputStream类包含用于序列化对象的writeObject()方法.
ObjectInputStream类包含用于反序列化对象的readObject()方法.
4.一些值得值得注意的点 - 仅非静态数据成员通过序列化过程保存.
- 静态数据成员和临时数据成员不会通过序列化过程保存.因此,如果不想保存非静态数据成员的值,请使用transient关键字修饰之使其变为临时性,比如一些用户敏感信息(如密码,身份证号等)不想在网络操作中被传输,就应该让其变成临时性数据成员.
- 如果父类实现了Serialable接口,那么继承自其的子类无需再实现,反之亦然.
- 反序列化对象时,永远不会调用对象的构造函数.(这一点还需要去想想原因,知其然,不知其所以然,欢迎评论区指教)
- 关联对象必须实现Serializable接口.
5.Talk is cheap,show me the code.
Person.java(正常序列化)
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
public Person(){}
private String username;
private int age;
private long id;
public String getUsername() {
return username;
}
public int getAge() {
return age;
}
public long getId() {
return id;
}
public void setUsername(String username) {
this.username = username;
}
public void setAge(int age) {
this.age = age;
}
public void setId(long id) {
this.id = id;
}
}
SerialTest.java
public class SerialTest {
public static void main(String[] args) {
Person person = new Person();
person.setUsername("lzy");
person.setAge(22);
person.setId(1238272922);
System.out.println("未序列化之前的username: " + person.getUsername());
System.out.println("未序列化之前的age: " + person.getAge());
System.out.println("未序列化之前的id: " + person.getId());
try {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("/Users/cr7mr/code/java/offer/Person.rtf"));
outputStream.writeObject(person);
outputStream.flush();
outputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
try {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("/Users/cr7mr/code/java/offer/Person.rtf"));
person = (Person)objectInputStream.readObject();
objectInputStream.close();
System.out.println("\n序列化之后的username: " + person.getUsername());
System.out.println("序列化之后的age: " + person.getAge());
System.out.println("序列化之后的id: " + person.getId());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
运行截图:
Person.java(使用transient关键字修饰后进行序列化)
运行截图:
可以看到Person对象反序列化后,其被transient修饰的id属性没有值,说明该属性根本没有被序列化.
Person.java(静态变量的序列化)
SerialTest.java(在反序列化之前修改静态变量age的值)
运行截图:
可以看到age是99,这说明反序列化后类中static型变量age的值为当前JVM中对应static变量的值,是修改后的99,而不是序列化时的22.说明staitic修饰的变量,是不会被序列化的.
6.我们为何要用序列化
序列化当然不是为了解决「我给朋友看我的猫」的问题,黑格尔在《法哲学原理》里面说说「存在即合理」序列化的存在,一定还是有其优势滴.
- [1 ] 保存或者保留对象的状态
- [2 ] 在网络上传输对象
一图以言之:
7.还想知道的事
「反序列化对象时,永远不会调用对象的构造函数」,那是如何创建反序列化后的对象的呢?希望有大佬解惑.