什么是序列化和反序列化
Java提供了一种对象序列化机制,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。
将序列化写入文件后,可以从文件中读取出来,并且对它重新进行反序列化,也就是说,对象的类型信息、对象的数据和对象中的数据类型可以用来在内存中新建对象。
因此我们可以通过上边所述定义序列化和反序列化的概念:
- 序列化:把对象转换为字节序列的过程叫做对象的序列化。
- 反序列化:把字节序列恢复为对象的过程称为对象的反序列化。
什么时候需要序列化
序列化的使用肯定与保存对象有关,大致有以下几种情况需要序列化:
- 当你想把内存中的对象状态保存到一个文件中或者数据库中
- 当你想用套接字在网络上传送对象时
- 当你想通过RMI传输对象的时候(Java RMI,即 远程方法调用(Remote procedure call),一种用于实现远程过程调用(RPC)(Remote procedure call)的Java API, 能直接传输序列化后的Java对象和分布式垃圾收集)
如何实现序列化
实现序列化其实很简单,只要实现Serializable接口就可以了。
让我们来测试一下序列化。
首先写一个要实现序列化的对象。
import java.io.Serializable;
public class Person implements Serializable{
private static String country = "China";
private String name;
private Integer age;
transient private String sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
//public String getAddTip() {
// return addTip;
//}
//
//public void setAddTip(String addTip) {
// this.addTip = addTip;
//}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", sex=" + sex + ", country=" + country + "]";
}
}
我们手写一个序列化方法和反序列化方法来测试
import java.io.*;
public class TestMain {
public static void main(String[] args) throws Exception{
serialzePerson();
Person person = deserializaPerson();
System.out.println(person.toString());
}
private static void serialzePerson() throws IOException{
Person person = new Person();
person.setName("Wyong");
person.setAge(20);
person.setSex("男");
//对象输出流,writeObject方法可以对一个对象序列化,将字节序列写到一个目标输出流中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:/code/eclipse/SerializableTest/person.txt")));
oos.writeObject(person);
System.out.println("Person对象序列化成功");
oos.close();
}
private static Person deserializaPerson() throws Exception{
//对象输入流,readObject方法从一个输入流中读取字节序列,再反序列化为一个对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:/code/eclipse/SerializableTest/person.txt")));
Person person = (Person) ois.readObject();
System.out.println("Person对象反序列化成功!");
return person;
}
}
跑一遍先!
从运行结果我们可以看出:
- 基本实现了对象的序列化和反序列化。
- transient修饰的属性是不会被序列化的。
可能有人会问,那我们如何验证static变量contry是否能够序列化。那么我们接下来再做一个小测试。
我们先进行一次序列化(不进行反序列化),然后修改代码中的country为china,然后直接进行反序列化。结果如下图:
很明显,country是没有进行序列化的。
首先我们要明白,序列化只会序列化堆内存中的信息,被static被修饰的变量并不是存放在堆上,而是在方法区,因此static是无法被实例化的。
那么又有人会问为什么country还能读到值,不应该是为null或者没有这个字段吗?这是因为静态变量在方法区,文件中是不存在country这个字段的,只是我们在打印静态变量时,JVM会自动去方法区里找,所以才能找到有值。
默认序列化机制
如果仅仅只是让某个类实现Serializable接口,而没有其他任何处理的话,则就是使用默认序列化机制。使用默认机制,在序列化对象时,不仅会序列化当前对象本身,还会对该独享引用的其他对象也进行序列化,同样的,这些其他对象引用的另外对象也将被序列化,以此类推。所以,如果一个对象包含的成员变量时容器类对象,而这些容器所包含的元素也是容器类对象,那么这个序列化过程就会比较复杂,且开销比较大。
关于serialVersionUID
当我们实现Serializable接口时,编译器会弹出一条警告
序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException。可序列化类可以通过声明名为 “serialVersionUID” 的字段(该字段必须是静态 (static)、最终 (final) 的 long 型字段)显式声明其自己的 serialVersionUID:
如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值,如“Java™ 对象序列化规范”中所述。不过,强烈建议 所有可序列化类都显式声明 serialVersionUID 值,原因是计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。因此,为保证 serialVersionUID 值跨不同 java 编译器实现的一致性,序列化类必须声明一个明确的 serialVersionUID 值。还强烈建议使用 private 修饰符显示声明 serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类 – serialVersionUID 字段作为继承成员没有用处。数组类不能声明一个明确的 serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配 serialVersionUID 值的要求。
transient关键字
既然上边提到了transient就讲一讲这个关键字。transient这个关键字比较简单,主要也是用在序列化之中,被transient修饰的变量无法被序列化,本地变量、方法和类不能被transient关键字修饰。
那么什么时候我们需要使用这个关键字呢?例如在网络信息传输的过程中,有一些用户的敏感信息不希望在网络操作中被传输,我们就可以加上transient关键字,避免序列化操作。
另一种实现序列化的方式
无论是使用transient关键字,还是使用writeObject()和readObject()方法,其实都是基于Serializable接口的序列化。JDK中提供了另外一个序列化接口Externalizable,使用这个接口之后,之前基于Serializable接口的序列化机制就将失效。
让我们修改一下Person类
import java.io.EOFException;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class Person implements Externalizable{
private static String country = "china";
private String name;
private Integer age;
transient private String sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public static String getCountry() {
return country;
}
public static void setCountry(String country) {
Person.country = country;
}
//public String getAddTip() {
// return addTip;
//}
//
//public void setAddTip(String addTip) {
// this.addTip = addTip;
//}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", sex=" + sex + ", country=" + country + "]";
}
public Person() {
System.out.println("调用无参构造函数");
}
public Person(String name, Integer age, String sex) {
super();
this.name = name;
this.age = age;
this.sex = sex;
System.out.println("调用有参构造");
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
out.writeObject(sex);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
sex = (String) in.readObject();
}
}
接着是测试类
import java.io.*;
public class TestMain {
public static void main(String[] args) throws Exception{
System.out.println("开始写入文件进行序列化");
writeToFile();
System.out.println("序列化成功!\n开始反序列化!");
writeFromFile();
System.out.println("反序列化成功");
}
private static void writeToFile() throws FileNotFoundException, IOException{
Person person = new Person("Wyong",20,"男");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:/code/eclipse/SerializableTest/person2.txt")));
person.writeExternal(oos);
oos.close();
}
private static void writeFromFile() throws FileNotFoundException, IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:/code/eclipse/SerializableTest/person2.txt")));
Person newPerson = new Person();
newPerson.readExternal(ois);
ois.close();
System.out.println(newPerson.toString());
}
}
Run一遍,结果如下:
Externalizable继承于Serializable,当使用该接口时,序列化的细节需要由程序员去完成。若使用Externalizable进行序列化,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。这就是为什么在此次序列化过程中Person类的无参构造器会被调用。由于这个原因,实现Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public。
Serializable和Externalizable的不同
- Serializable序列化时不会调用默认的构造器,而Externalizable序列化时会调用默认构造器。
- Serializable会自动序列化所有可以序列化的属性,若不想要序列化则使用transient关键字修饰。而Externalizable是可以只序列化指定的属性(调用writeExternal()和readExternal()方法),默认不保存对象的任何字段。
- Serializable可以使用transient避免序列化,Externalizable使用了transient虽然不会有错误,但也不起作用。
参考:https://blog.csdn.net/u013870094/article/details/82765907