系列化-反序列化
序列化—反序列化概念
什么是系列化,反序列化
- 对象序列化,将对象以二进制的形式保存在硬盘上
- 反序列化,将二进制的文件转化为对象读取
Java序列化就是指把Java对象转换为字节序列的过程
Java反序列化就是指把字节序列恢复为Java对象的过程
系列化,反序列化的作用
- 序列化最重要的作用:在传递和保存对象时.保证对象的完整性和可传递性,对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。
- 反序列化的最重要的作用:根据字节流中保存的对象状态及描述信息,通过反序列化重建对象。
总结:核心作用就是对象状态的保存和重建。(整个过程核心点就是字节流中所保存的对象状态及描述信息)
系列化使用场景
场景:
1、当想把的内存中的对象状态保存到一个文件中或者数据库中表(内存到数据库)
2、当想用套接字在网络上传送对象(网络传输)(或者不同线程之间通讯)
3、当想通过RMI传输对象的时候(分布式传输)
序列化的目的有两个,第一个是便于存储,第二个是便于传输。我们一般的实体类不需要程序员再次实现序列化的时候,请想两个问题:第一:存储媒体里面,是否是有其相对应的数据结构?第二:这个实体类,是否需要远程传输(或者两个不同系统甚至是分布式模块之间的调用)
在对对象进行实例化的过程中相关注意事项
a)序列化时,只对对象的状态进行保存,而不管对象的方法;
b)当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
c)当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
d)并非所有的对象都可以序列化,至于为什么不可以,有很多原因了,比如:
1.安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行RMI传输 等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的。
2. 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现
序列化机制的解决方案:
1.保存到磁盘的所有对象都获得一个序列号(1, 2, 3等等)
2.当要保存一个对象时,先检查该对象是否被保存了。
3.如果以前保存过,只需写入"与已经保存的具有序列号x的对象相同"的标记,否则,保存该对象通过以上的步骤序列化机制解决了对象引用的问题!
json/xml的数据传递:
在数据传输(也可称为网络传输)前,先通过序列化工具类将Java对象序列化为json/xml文件。
在数据传输(也可称为网络传输)后,再将json/xml文件反序列化为对应语言的对象
序列化优点:
①将对象转为字节流存储到硬盘上,当JVM停机的话,字节流还会在硬盘上默默等待,等待下一次JVM的启动,把序列化的对象,通过反序列化为原来的对象,并且序列化的二进制序列能够减少存储空间(永久性保存对象)。
②序列化成字节流形式的对象可以进行网络传输(二进制形式),方便了网络传输。
③通过序列化可以在进程间传递对象
为什么保存到文件中要序列化
当我们想把的内存中的对象状态保存到一个文件中或者数据库中时候,Serializable接口能帮我们做些什么
如上所述,读写对象会有什么问题呢?比如:我要将对象写入一个磁盘文件而后再将其读出来会有什么问题吗?别急,其中一个最大的问题就是对象引用!举个例子来说:假如我有两个类,分别是A和B,B类中含有一个指向A类对象的引用,现在我们对两个类进行实例化{ A a = new A(); B b = new B(); },这时在内存中实际上分配了两个空间,一个存储对象a,一个存储对象b,接下来我们想将它们写入到磁盘的一个文件中去,就在写入文件时出现了问题!因为对象b包含对对象a的引用,所以系统会自动的将a的数据复制一份到b中,这样的话当我们从文件中恢复对象时(也就是重新加载到内存中)时,内存分配了三个空间,而对象a同时在内存中存在两份,想一想后果吧,如果我想修改对象a的数据的话,那不是还要搜索它的每一份拷贝来达到对象数据的一致性,这不是我们所希望的!
Java实现序列化和反序列化的过程
实现序列化的必备要求:
只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)
Serializable接口
实现对象序列化需要做哪些工作
- 实现serializable接口,不想让字段放在硬盘上就加transient
public class Person implements Serializable {}
其实这是一个空接口,并没有需要去实现的方法,相当于一个标识,可以被序列化。
一个java中的类只有实现了Serializable接口,它的对象才是可序列化的
如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient(暂态的) [ˈtrænzɪənt] 关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化
transient(暂态的)
java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问
2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口
3)被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。
serialVersionUID
serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的。如果对类的源代码作了修改,再重新编译,新生成的类文件的serialVersionUID的取值有可能也会发生变化。
类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的serialVersionUID,也有可能相同。为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示的定义serialVersionUID,为它赋予明确的值。
显式地定义serialVersionUID有两种用途:
a. 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
b. 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。
private static final long serialVersionUID = 1L; 的意义
语句本身可能就是显示指定了UID的值,但是为什么看项目中都是给定了1L呢?
首先,不给定的话因为不同的JVM之间的序列化算法是不一样的,不同的JDK也可能不一样,不利于程序的移植,可能会让反序列化失败
实现java.io.Serializable这个接口是为序列化,serialVersionUID 用来表明实现序列化类的不同版本间的兼容性。如果你修改了此类, 要修改此值。否则以前用老版本的类序列化的类恢复时会出错。
但是为什么好多都给定的是1呢
你可以随便写一个,在Eclipse中它替你生成一个,有两种生成方式: 一个是默认的1L,比如:private static final long serialVersionUID = 1L; 一个是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:private static final long serialVersionUID = -8940196742313994740L;之类的
JDK中序列化和反序列化的API:
- java.io.ObjectInputStream:对象输入流该类的readObject()从输入流中读取字节序列,然后将字节序列反序列化为一个对象并返回**
- java.io.ObjectOutputStream:对象输出流该类的writeObject(Object obj)将 将传入的object对象进行序列化,把得到的字节序列写入到目标输出流中进行输出
// 将list数据,序列化到硬盘文件中
public String objectStream(List<Student> list) {
try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(读取的文件位置))){
// 硬盘数据反序列化到内存中
List<Student> origin = ois.readObject();
// 创建目标集合,序列化到硬盘文件中
List<Student> target = new ArrayList<>();
for(Student s : origin) {
target.add(s);
}
// 创建对象输出流,将target序列化到硬盘文件中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(读取的文件位置));
oos.wirteObject(target);
return "list成功序列化";
}catch(Exception e) {
e.printStack();
}finally {
// 关闭对象输出流
// 偷个懒
oos.close();
}
}
这里解释一下:代码中为什么要创建一个target集合,为什么要先反序列化读入内存中而不直接序列化到硬盘文件中去
因为我们不能够保证硬盘文件中是否已经存在数据,如果直接将数据序列号到硬盘文件中,里面的内容就会全部覆盖掉,所以创建一个target集合,保存原来的数据,最后将我的新添加的数据,添加到target集合里,再使用对象输出流序列化到硬盘文件中。这样我们的数据就相当于追加到文件内容的最后面
实现序列化和反序列化的三种实现:
- 若Student类仅仅实现了Serializable接口,则可以按照以下方式进行序列化和反序列化。
①ObjectOutputStream采用默认的序列化方式,对Student对象的非transient的实例变量进行序列化
②ObjcetInputStream采用默认的反序列化方式,对Student对象的非transient的实例变量进行反序列化 - 若Student类仅仅实现了Serializable接口,并且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),
则采用以下方式进行序列化与反序列化:
①ObjectOutputStream调用Student对象的writeObject(ObjectOutputStream out)的方法进行序列化。
②ObjectInputStream会调用Student对象的readObject(ObjectInputStream in)的方法进行反序列化。 - 若Student类实现了Externalnalizable接口,且Student类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,
则按照以下方式进行序列化与反序列化:
①ObjectOutputStream调Student对象的writeExternal(ObjectOutput out))的方法进行序列化
②ObjectInputStream会调用Student对象的readExternal(ObjectInput in)的方法进行反序列化
序列化和反序列化代码示例
flush()方法理解:flush()这个函数是清空的意思,用于清空缓冲区的数据流,进行流的操作时,数据先被读到内存中,然后再用数据写到文件中,那么当你数据读完时,我们如果这时调用close()方法关闭读写流,这时就可能造成数据丢失,为什么呢,因为,读入数据完成时不代表写入数据完成,一部分数据可能会留在缓存区中
public class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//序列化
FileOutputStream fos = new FileOutputStream("object.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
Student student1 = new Student("lihao", "wjwlh", "21");
oos.writeObject(student1);
oos.flush();
oos.close();
//反序列化
FileInputStream fis = new FileInputStream("object.out");
ObjectInputStream ois = new ObjectInputStream(fis);
Student student2 = (Student) ois.readObject();
System.out.println(student2.getUserName()+ " " +
student2.getPassword() + " " + student2.getYear());
}
}
public class Student implements Serializable{
private static final long serialVersionUID = -6060343040263809614L;
private String userName;
private String password;
private String year;
public String getUserName() {
return userName;
}
public String getPassword() {
return password;
}
public void setUserName(String userName) {
this.userName = userName;
}
public void setPassword(String password) {
this.password = password;
}
public String getYear() {
return year;
}
public void setYear(String year) {
this.year = year;
}
public Student(String userName, String password, String year) {
this.userName = userName;
this.password = password;
this.year = year;
}
}