我们在科幻电影中,经常能够看到“瞬间传送”这种神奇的科技。在生活中,尤其是在上下班和春节回家的时候,我也是真的想体验一下“瞬间传送”啊!从科学角度来讲,“瞬间传送”的本质应该是先将被传送的物质,分解为物质的最小单位夸克(已知最小),然后将这些夸克传送到目标位置,再根据夸克之前的排列顺序进行重组,最后被传送的物质就这样被重构出现在很遥远的另一个地方了。
在Java中,也有一种类似的“瞬间传送”,它就是对象的序列化和反序列化。之所以这么说,是因为序列化就像是传送前,对被序列化对象进行分解为字节单位、并传送至指定位置的过程(磁盘中指定的位置);而反序列化就是将距离很遥远的目标(磁盘中序列化对象文件的位置)进行分解传送到眼前,然后进行根据顺序重新排列组合字节码,将其恢复为该对象的过程。
将需要保存的对象持久化到磁盘上,使得对象可以脱离运行程序而独立存在,在需要的时候通过反序列化重新恢复为对象。
1. 对象持久化:可通过对象序列化将对象永久保存到磁盘上,通常是保存到文件上 。
2. 网络传输:通过将对象转换为字节序列,可以将其从主机A传输到主机B上,然后在主机B上反序列化重构出对象 。
所有可在网络上传输的对象都必须是可序列化的,如远程方法调用时,所有的参数对象都必须是可序列化的, 否则会出现异常 。所以我们在 创建Bean类的时候,最好都实现Serializable序列化接口 。
在Java中,想要 实现序列化有两种方式 ,分别是 实现Serializable接口(自动序列化)或实现Externalizable接口(手动强制序列化) 。
/** * 学生类 实现Serializable序列化接口 * 用于验证在对象序列化时,瞬时变量和静态变量是否能够被持久化的问题 * 瞬时变量:被transient修饰的变量,其生命周期在调用者内存中,无法被序列化(持久化),在序列化时其值会丢失 * 静态变量:被static修饰的变量,其已经属于该类,不再属于类对象,所以即便是没有被transient修饰,其也无法被持久化 */ public class Student implements Serializable { /** * 当类实现了Serializable接口时,类中所有的元素都将自动序列化,无需手动添加 */ private static transient String studentID;//编号 非瞬时变量(被static修饰的变量,无论是否被transient修饰,都不会被序列化;用于验证,不用在意) private String studentName;//姓名 private String studentAge;//年龄 private String studentAddress;//住址 private transient String studentIDCard;//身份证号 瞬时变量(transient修饰),在序列化的时候不会被持久化到磁盘上 private transient String studentPhone;//联系方式 瞬时变量一般用于用户敏感信息安全,避免在持久化的时候泄露 private String studentAccount;//分数 public Student(String name, String studentName, String studentAge, String studentAddress, String studentIDCard, String studentPhone, String studentAccount) { this.studentName = studentName; this.studentAge = studentAge; this.studentAddress = studentAddress; this.studentIDCard = studentIDCard; this.studentPhone = studentPhone; this.studentAccount = studentAccount; } //省略get()和set()方法 @Override public String toString() { return "Student{" + "studentName='" + studentName + '/'' + ", studentAge='" + studentAge + '/'' + ", studentAddress='" + studentAddress + '/'' + ", studentIDCard='" + studentIDCard + '/'' + ", studentPhone='" + studentPhone + '/'' + ", studentAccount='" + studentAccount + '/'' + '}'; } }
实现Serializable接口的时候不用重写任何方法,即可直接自动序列化(无需手动指定需要序列化的元素,被transient修饰的 瞬态变量 和被static修饰的静态变量除外) 。在使用Serializable接口序列化的时候,先创建一个 ObjectOutputStream 输入流对象并指定文件路径,然后将需要序列化的类对象放入输入流对象的 writeObject()方法 即可。
/** * 序列化工具类 * 用于类对象持久化 */ public class SerializationUtil { /** * 序列化 */ public static void writeObject(String fileName, Serializable serializable){ try { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName)); oos.writeObject(serializable); oos.close(); } catch (IOException e) { e.printStackTrace(); }catch (Exception e){ e.printStackTrace(); } } /** * 反序列化 */ public static Object readObject(String fileName){ Object object = null; try { ObjectInput oi = new ObjectInputStream(new FileInputStream(fileName)); object = oi.readObject(); oi.close(); } catch (IOException e) { e.printStackTrace(); } catch (Exception e){ e.printStackTrace(); } return object; } }
编写测试类进行测试:
/** * 学生类测试类 * 用于验证Serializable序列化时,瞬时变量和静态变量的取值问题 */ public class SerializationTest { public static void main(String[] args) { Student student = new Student("1","张三","22", "上海市浦东新区","410804199803120341", "13763412233","89"); //序列化 SerializationUtil.writeObject("E:/programming/serializTest.txt",student); //反序列化 Student sd = (Student) SerializationUtil.readObject("E:/programming/serializTest.txt"); //studentID为静态变量,不可被持久化,studentIDCard和studentPhone为瞬时变量,不可持久化 System.out.println(sd.toString()); } }
运行结果:
创建一个Peoplo类,用于验证serializable接口中的自定义序列化
/** * 普通人类 实现Serializable序列化接口 * 用于验证Serializable中的自定义序列化方式 */ public class People implements Serializable { private static String peopleID;//编号(stataic纯粹用于验证自定义序列化时,静态变量的序列化,不用在意) private String peopleName;//姓名 private int age;//年龄 private String peopleAddress;//住址 private transient String peoplePhone;//联系方式 瞬态变量 //无参构造 用于反序列化时反射构造对象 public People() { } //有参构造,用于初始化对象 public People(String peopleName, int age, String peopleAddress, String peoplePhone) { this.peopleName = peopleName; this.age = age; this.peopleAddress = peopleAddress; this.peoplePhone = peoplePhone; } //省略get()方法和set()方法 @Override public String toString() { return "People{" + "peopleID='" + peopleID + '/'' + ", peopleName='" + peopleName + '/'' + ", age=" + age + ", peopleAddress='" + peopleAddress + '/'' + ", peoplePhone='" + peoplePhone + '/'' + '}'; } //重写writeObject(ObjectOutputStream out)和readObject(ObjectInputStream ins)方法 private void writeObject(ObjectOutputStream out) throws IOException{ out.writeObject(this.peopleName); out.writeInt(this.getAge()); out.writeObject(this.peoplePhone); } private void readObject(ObjectInputStream ins) throws IOException,ClassNotFoundException{ this.peopleName = (String)ins.readObject(); this.age = ins.readInt(); this.peoplePhone = (String)ins.readObject(); } }
编写测试类PeopleTest
/** * 普通人类测试类 用于验证Serializable中的自定义序列化方式 */ public class PeopleTest { public static void main(String[] args) { People people = new People("王二麻子",45,"上海市闵行区","15567341990"); //序列化对象 SerializationUtil.writeObject("E:/programming/serializTest.txt",people); //反序列化对象 People pp = (People) SerializationUtil.readObject("E:/programming/serializTest.txt"); System.out.println(pp.toString()); } }
运行结果:
解释:虽然peoplePhone属性是被transient关键字修饰的瞬态变量,但是因为我们在重写的writeObject(ObjectOutputStream out)和readObject(ObjectInputStream ins)方法中,指定了peoplePhone属性需要被强制序列化,所以在运行结果中,peoplePhone仍旧可以取到值。而没有被transient关键字修饰的peopleAddress属性,正常来说是能被序列化的,但是在重写的两个方法中没有定义该属性要被强制序列化和反序列化,所以我们在运行结果中取不到值(自动显示String类型的默认值)。
//这个方法会在序列化时,调用writeObject()方法之前调用; //该方法和writeObject()方法不能同时写,会报错 private Object writeReplace() throws ObjectStreamException{ ArrayList<Object> list = new ArrayList<>(4); list.add(this.peopleName); list.add(this.age); list.add(this.peopleAddress); list.add(this.peoplePhone); return list; } //这个方法会在反序列化时,调用readObject()方法之前调用,用于替换反序列化的对象,原解析对象会被立刻丢弃; // 该方法和readObject()方法不能同时写,会报错 private Object readResolve() throws ObjectStreamException { return new People("张三丰子",33,"上海市黄浦区","17734349989"); } //注意:writeObject(ObjectOutput out)方法和writeReplace()方法不能同时重写,同样, // readObject (ObjectInput ins)方法和readReplace()方法也不能同时重写,编译时会报错。
创建Teacher类并实现Externalizable接口
/** * 教师类 实现Externalizable序列化接口 * 用于验证在对象序列化时,瞬时变量和静态变量是否能够被持久化的问题,同Student类 */ public class Teacher implements Externalizable { /** * 当类实现了Externalizable接口后,类中所有元素均不会自动序列化,需要在writeExternal()方法中指定 * 但是和Serializable接口不同,该序列化接口下,transient修饰符修饰的变量可被序列化 */ private static transient String teacherID;//编号 非瞬时变量(被static修饰的变量,无论是否被transient修饰,都不会被序列化) private String teacherName;//姓名 private String teacherAge;//年龄 private String teacherAddress;//住址 private transient String teacherIDCard;//身份证号 瞬时变量(transient修饰),在序列化的时候不会被持久化到磁盘上 private transient String teacherPhone;//联系方式 瞬时变量一般用于用户敏感信息安全,避免在持久化的时候泄露 //必须有无参构造器(否则会报无可用的构造器异常),用于在反序列化的时候反射创建对象 public Teacher() {} public Teacher(String teacherName, String teacherAge, String teacherAddress, String teacherIDCard, String teacherPhone) { this.teacherName = teacherName; this.teacherAge = teacherAge; this.teacherAddress = teacherAddress; this.teacherIDCard = teacherIDCard; this.teacherPhone = teacherPhone; } //省略get()和set()方法 @Override public String toString() { return "Teacher{" + "teacherName='" + teacherName + '/'' + ", teacherAge='" + teacherAge + '/'' + ", teacherAddress='" + teacherAddress + '/'' + ", teacherIDCard='" + teacherIDCard + '/'' + ", teacherPhone='" + teacherPhone + '/'' + '}'; } //实现Externalizable接口的时候必须重写该接口的writeExternal(ObjectOutput out)方法和readExternal(ObjectInput in)方法。 //writeExternal(ObjectOutput out):方法用于指定需要强制序列化的元素; @Override public void writeExternal(ObjectOutput out) throws IOException { //在此指定需要序列化的元素,使用反转加密方式 out.writeObject(new StringBuffer(teacherName).reverse()); out.writeObject(new StringBuffer(teacherAge).reverse()); out.writeObject(new StringBuffer(teacherAddress).reverse()); out.writeObject(new StringBuffer(teacherIDCard).reverse());//虽被transient修饰,但因在此指定,所以仍可序列化 out.writeObject(new StringBuffer(teacherPhone).reverse());//虽被transient修饰,但因在此指定,所以仍可序列化 } //readExternal(ObjectInput in):方法用于指定需要反序列化的元素; @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { //在此指定需要反序列化的数据,需反转后取出 this.teacherName = ((StringBuffer)in.readObject()).reverse().toString(); this.teacherAge = ((StringBuffer)in.readObject()).reverse().toString(); this.teacherAddress = ((StringBuffer)in.readObject()).reverse().toString(); this.teacherIDCard = ((StringBuffer)in.readObject()).reverse().toString(); this.teacherPhone = ((StringBuffer)in.readObject()).reverse().toString(); } }
测试类:
/** * 教师类测试类 * 用于验证Externalizable序列化时,瞬时变量和静态变量的取值问题 */ public class ExternalizableTest { public static void main(String[] args) { Teacher teacher = new Teacher("语文老师","33","上海市松江区", "410804198402120445","13723324546"); //序列化 SerializationUtil.writeObject("E:/programming/serializTest.txt",teacher); //反序列化 Teacher tc = (Teacher) SerializationUtil.readObject("E:/programming/serializTest.txt"); //虽然teahcer类中也有静态变量和瞬时变量,但是静态变量不可被序列化(异常),瞬时变量只要被指定,即可被强制序列化 System.out.println(tc.toString()); } }
运行结果:
解释:虽然Teacher类中,也有被transient修饰的瞬态变量,但是因为Externalizable接口中所有需要序列化和反序列化的属性都需要手动指定,所以transient关键字相当于无效,因此我们可以看到反序列化读取对象的时候,所有的属性都被打印了出来。
在Java中,如果要对统一对象进行多次序列化操作,那么结果就是:该对象只会被序列化一次。这是因为,在Java的序列化机制中,对象在序列化之后,会保存一个序列化编码,如果这个对象已经被序列化过了,那么会直接调用该编码,而不是重新序列化。
这样的操作有其 优点:避免同一对象被多次序列化,提高了性能;
但是也有其 缺点:如果该对象在被序列化之后,做出了修改,则再次序列化很有可能无法更新磁盘中的对象。
我们在使用 部分开发工具 的时候(如MyEclipse等),会提示我们要 重写Serializable接口的serialVersionUID静态常量属性 (也可以不重写,不会出现异常)。SerializVersionUID的作用在于,当我们在升级项目的时候,反序列化使用的class文件肯定也要升级,为了保证升级后序列化的兼容性,我们可以使用序列化版本号 serializVersionUID来保证序列化和反序列化的正确性 ,只要serializVersionUID相同,那么即便是我们更改了序列化的属性,我们也可以正确的反序列化。
private static final long serialVersionUID = 8294180014912103005L;
SerializVersionUID的生成是Java运行时环境根据类的内部细节自动生成的。若是 在没有显式定义SerializVersionUID的情况下,对类进行修改,则在反序列化的时候会抛出异常 ,这是因为我们在 修改类的时候,产生了新的SerializVersionUID(生成细节已改变),新旧SerializVersionUID不相同,反序列化的时候SerializVersionUID对不上,所以自然就出现了异常 。
需要更改SerializVersionUID的情况:
强烈建议在实现了序列化接口的类中,显式定义SerializVersionUID,这样在修改了类中的属性和方法之后,反序列化也不会出错。