1. 定义
序列化:把对象转换为字节序列的过程称为对象的序列化。将JVM堆中对象以文件的方式保存下来。
反序列化:把字节序列恢复为对象的过程称为对象的反序列化。将文件加载进内存,恢复到序列化之前的状态。
2. 序列化场景
将内存中的对象状态保存到数据库或文件中;
对象在网络中以套接字进行传送;
通过RMI传送对象。
3. 实现序列化
ObjectOutputStream:对象输出流,writeObject(Object obj)可以指定obj对象序列化,并把得到的字节写到目标输出流。
ObjectInputStream:对象输入流,readObject()可以从源输入流读取字节序列,反序列化成对象,将其返回。
只有实现了Serializable,Externalizable接口的类的对象才能被序列化。
Serializable:接口中没有具体方法。会采用默认的序列化方式。
Externalizable:继承自Serializable,如果实现Externalizable接口需要类本身重写writeExternal(ObjectOutput out)和readExternal(ObjectInput in),完全由类本身控制序列化行为。
方法的重写较为简单,需要在方法小红分别out.writeXxx();和 in.readXxx()将字段写入和读出,注意写入的顺序和读出的顺序可以一致,因为对象装载是有序的。
3.1 实体类实现Serializable接口实现序列化
/** * 用户类,实现Serializable接口 */ public class User implements Serializable { private String username; private String password; private int age; public User() { } public User(String username, String password, int age) { this.username = username; this.password = password; 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; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "User{" + "username='" + username + '\'' + ", password='" + password + '\'' + ", age=" + age + '}'; } }
public class Main { /** * 序列化对象 */ @Test public void serializeUser() throws IOException { User user= new User("haha","123456",20); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/12.txt")); oos.writeObject(user); System.out.println("序列化对象成功"); } /** * 反序列化对象 */ @Test public void deSerializeUser() throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/12.txt")); User user = (User)ois.readObject(); System.out.println(user.getUsername()); System.out.println(user.getPassword()); System.out.println(user.getAge()); } }
3.2 实体类实现Externalizable接口实现序列化
测试类和实现Serializable接口测试类一致,用户类中仅多writeExterbal和readExternal方法
/** * 用户类,实现Externalizable接口 */ public class User implements Externalizable { private String username; private String password; private int age; public User() { } public User(String username, String password, int age) { this.username = username; this.password = password; 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; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "User{" + "username='" + username + '\'' + ", password='" + password + '\'' + ", age=" + age + '}'; } @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(username); out.writeObject(password); out.writeObject(age); } /** * * 如果你写反了属性读取的顺序,你可以发现反序列化的读取的对象的指定的属性值也会与你写的读取方式一一对应。因为在文件中装载对象是有序的 */ @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { password = (String) in.readObject(); age = (int) in.readObject(); username = (String) in.readObject(); } }
4. serialVersionUID的作用
serialVersionUID: 序列化的版本号,实现Serializable接口的类都有一个表示序列化版本标识符的常量。
idea可以设置如果实体类实现Serializable让系统生成一个serialVersionUID,具体做法链接。
在上面两个实体类中加入一个性别字段(sex),如果实现Serializable接口且不存在serialVersionUID这个静态变量,则会抛出java.io.InvalidClassException异常;如果实现Externalizable且不存在serialVersionUID这个静态变量也会抛出异常。
抛出的异常如下,意思是流和本地的class中的serialVersionUID不一致,虚拟机基于安全机制考录,抛出异常,并且拒绝载入。
local class incompatible: stream classdesc serialVersionUID = -4043995571061330238, local class serialVersionUID = 3451805080334762168
serialVersionUID:保证类的唯一性,即使加空格前后类的serialVersionUID也会不一样。
private static final long serialVersionUID = -1157905086552887895L;
在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。如果不显示指定uid,编译器会为我们自动生成uid,两次自动生成的uid不一致就会抛异常。
注意:transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。静态变量在反序列化中存在值,只能说明是读取JVM中得值。例子如下。
//调整变量修饰符
public static String username; private transient String password;
public class Main { /** * 序列化对象 */ @Test public void serializeUser() throws IOException, ClassNotFoundException { User user= new User(); User.username="username"; user.setAge(20); user.setPassword("password"); System.out.println("序列化前 username=:"+user.getUsername()); System.out.println("序列化前 age=:"+user.getAge()); System.out.println("序列化前 password=:"+user.getPassword()); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/12.txt")); oos.writeObject(user); System.out.println("序列化对象成功"); System.out.println("----------反序列化---------------------"); User.username="xiaohua"; ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/12.txt")); User users = (User)ois.readObject(); System.out.println("序列化后 username=:"+users.getUsername()); System.out.println("序列化后 age=:"+users.getAge()); System.out.println("序列化后 password=:"+users.getPassword()); } }
通过结果可以看出username得值是读取得是jvm中静态变量,反序列化中流没有对应得值,只有在反序列化时指定username时值才会变。
序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中。
5. 哪此属性不会被序列化
如果该类有父类,则分两种情况来考虑,如果该父类已经实现了可序列化接口。则其父类的相应字段及属性的处理和该类相同;如果该类的父类没有实现可序列化接口,则该类的父类所有的字段属性将不会序列化,并且反序列化时会调用父类的默认构造函数来初始化父类的属性,而子类却不调用默认构造函数,而是直接从流中恢复属性的值。
如果该类的某个属性标识为static类型的,则该属性不能序列化。
如果该类的某个属性采用transient关键字标识,则该属性不能序列化。
在一个可序列化类中显示的定义serialVersionUID,为它赋予明确的值。