java的序列化和反序列化
序列化和反序列化的定义
Java序列化就是指把Java对象转换为字节序列的过程
Java反序列化就是指把字节序列恢复为Java对象的过程。
序列化最重要的作用:在传递和保存对象时,保证对象的完整性和可传递性。
对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。
反序列化的最重要的作用:根据字节流中保存的对象状态及描述信息,通过反序列化重建对象。
总结:核心作用就是对象状态的保存和重建,整个过程核心点就是字节流中所保存的对象状态及描述信息。
实现序列化和反序列化的2种实现
1.实现Serializable接口,readObject/writeObject
若Student类仅仅实现了Serializable接口。
并定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out)。
则采用以下方式进行序列化与反序列化。
ObjectOutputStream采用默认的序列化方式,自定义方法对Student对象的非transient的实例变量进行序列化。
ObjcetInputStream采用默认的反序列化方式,自定义方法对Student对象的非transient的实例变量进行反序列化。
注意使用默认的序列化 反序列化,只能对对象的非transient的实例变量进行序列化
2.实现Externalnalizable接口,readExternal/writeExternal
③若Student类实现了Externalnalizable接口,且Student类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,则按照以下方式进行序列化与反序列化。
ObjectOutputStream调用Student对象的writeExternal(ObjectOutput out))的方法进行序列化。
ObjectInputStream会调用Student对象的readExternal(ObjectInput in)的方法进行反序列化。
实现序列化的另外一种方式为实现接口Externalizable,Externlizable的部分源码例如以下:
没错,Externlizable接口继承了java的序列化接口,并添加了两个方法:
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
首先。我们在序列化对象的时候,因为这个类实现了Externalizable 接口,在writeExternal()方法里定义了哪些属性能够序列化。
哪些不能够序列化,所以。对象在经过这里就把规定能被序列化的序列化保存文件,不能序列化的不处理,然后在反序列的时候自己主动调
用readExternal()方法,依据序列顺序挨个读取进行反序列,并自己主动封装成对象返回,然后在測试类接收,就完毕了反序列。 所以说Exterinable的是Serializable的一个扩展。
package testReadObject;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;
class Person implements Externalizable {
private static final long serialVersionUID = 1L;
String userName;
String password;
String age;
public Person(String userName, String password, String age) {
super();
this.userName = userName;
this.password = password;
this.age = age;
}
public Person() {
super();
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
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;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
//添加一个新的对象
Date date = new Date();
out.writeObject(this.userName);
out.writeObject(this.password);
out.writeObject(this.age);
out.writeObject(this.date);
}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
//注意这里的接受顺序是有限制的哦,否则的话会出错的
// 比如上面先write的是A对象的话,那么以下先接受的也一定是A对象...
userName = (String) in.readObject();
System.out.println("反序列化后的用户名:" + userName);
password = (String) in.readObject();
System.out.println("反序列化后的密码:" + password);
age = (String) in.readObject();
System.out.println("反序列化后的年龄:" + age);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = (Date) in.readObject();
System.out.println("反序列化后的日期为:" + sdf.format(date));
}
@Override
public String toString() {
//年龄是没有被序列化,所以在反序列化的时候是读取不到数据的
return "username:" + userName + ",密 码:" + password + ",年龄:" + age;
}
}
public class Test {
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
Person person = new Person("小浩1", "123456", "21");
System.out.println("序列化之前的数据:\n" + person.toString());
ObjectOutput objectOutput = new ObjectOutputStream
(new FileOutputStream("a.txt"));
person.writeExternal(objectOutput);
System.out.println("------------------------------------------------");
ObjectInputStream ois=new ObjectInputStream
(new FileInputStream("a.txt"));
person.readExternal(ois);
}
}
Serializable接口、transient 关键字
一个类只有实现了serializable接口,它的对象才能被序列化
Serializable接口
实现该接口可以实现序列化,Serializable接口没有方法或字段,仅用于标识可序列化的语义。
但是,如果一个类没有实现这个接口,想要被序列化的话,抛出NotSerializableException异常
serialVersionUID
关于serialVersionUID,在日常项目中,我们可能会忽略,但是其作用不可小视。在实体类中,如果我们不加上serialVersionUID,可能会发生意想不到的错误。serialVersionUID其实是验证版本一致性的,在进行序列化和反序列化后,通过serialVersionUID来验证是否是同一个对象。
可以在IDEA中设置实现Serializable接口后自动生成。在实体类里面 实现 java.io.Serializable 接口,然后选中实体类名称 使用快捷键 Alt + Insert,选择“Adjust code style setting”,会生成serialVersionUID。自动生成的serialVersionUID与UUID类似,几乎不会重复。如
private static final long serialVersionUID = -8105780453909276536L;
假设没有明白指定serialVersionUID。序列化的时候会依据字段和特定的算法生成一个serialVersionUID,当属性有变化时这个id发生了变化,所以反序列化的时候
就会失败。抛出“本地classd的唯一id和流中class的唯一id不匹配”。
jdk文档关于serialVersionUID的描写叙述写道:
假设可序列化类未显式声明 serialVersionUID,则序列化执行时将基于该类的各个方面计算该类的默认 serialVersionUID 值,如“Java(TM) 对象序列化规范”中所述。只是,强烈建议 全部可序列化类都显式声明 serialVersionUID 值,原因是计算默认的 serialVersionUID 对类的具体信息具有较高的敏感性,依据编译器实现的不同可能千差万别。这样在反序列化过程中可能会导致意外的 InvalidClassException。
因此,为保证 serialVersionUID 值跨不同 java 编译器实现的一致性。序列化类必须声明一个明白的 serialVersionUID 值。
还强烈建议使用 private 修饰符显示声明 serialVersionUID(假设可能),原因是这样的声明仅应用于直接声明类 -- serialVersionUID 字段作为继承成员没实用处。
数组类不能声明一个明白的 serialVersionUID,因此它们总是具有默认的计算值,可是数组类没有匹配 serialVersionUID 值的要求。
结论
1.如果声明了serialVersionUID,那么即使序列化之后删除了某些字段再反序列化也是可以成功的。
2.如果没有声明serialVersionUID,那么序列化之后删除了某些字段再反序列化会报错,原因是没有声明serialVersionUID,serialVersionUID是根据类中的字段信息计算出来的。
测试方法:先执行序列化,然后去掉字段后在反序列化。
package test;
import java.io.*;
class Person implements Serializable {
private static final long serialVersionUID = 1L;
String userName;
String password;
String age;
public Person(String userName, String password, String age) {
super();
this.userName = userName;
this.password = password;
this.age = age;
}
public Person() {
super();
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
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;
}
@Override
public String toString() {
//注意这里的年龄是不会被序列化的,所以在反序列化的时候是读取不到数据的
return "username:" + userName + ",密 码:" + password + ",年龄:" + age;
}
}
public class Test {
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
/* Person person = new Person("小浩1", "123456", "21");
System.out.println("序列化之前的数据:\n" + person.toString());
ObjectOutput objectOutput = new ObjectOutputStream(new FileOutputStream("a.txt"));
objectOutput.writeObject(person);*/
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("a.txt"));
Person newPerson = (Person) ois.readObject();
System.out.println("-------------------------------------------------");
System.out.println("序列化之后的数据:\n" + newPerson.toString());
}
}
transient关键字
transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。此外,针对密码等敏感信息,也不应该进行序列化,因此需要在其前面加上 transient 关键字。
序列化相关的基础知识
java.io.ObjectOutputStream是实现序列化的关键类,它可以将一个对象转换成二进制流,然后可以通过ObjectInputStream将二进制流还原成对象。
主要的作用是用于写入对象信息与读取对象信息。 对象信息一旦写到文件上那么对象的信息就可以做到持久化了
1、需要序列化的类必须实现java.io.Serializable接口,否则会抛出NotSerializableException异常
2、如果检测到反序列化后的类的serialVersionUID和对象二进制流的serialVersionUID不同,则会抛出 异常。
3、Java的序列化会将一个类包含的引用中所有的成员变量保存下来(深度复制),所以里面的引用类型必须也要实现java.io.Serializable接口。
4、对于不采用默认序列化或无须序列化的成员变量,可以添加transient关键字,并不是说添加了transient关键字就一定不能序列化。
5、每个类可以实现readObject、writeObject方法实现自己的序列化策略,即使是transient修饰的成员变量也可以手动调用ObjectOutputStream的writeInt等方法将这个成员变量序列化。