java基础——序列化
为什么需要序列化
我们知道,java程序在运行时,对象是在对上创建的,如果程序停止了,那么这个对象也不复存在了。当我们需要将对象存储在硬盘上时,就需要序列化的技术了。
序列化就是一种将对象转换成字节序列的过程。反序列化就是将字节序列代表的对象恢复成原来的样子。通过序列化与反序列化,我们可以实现进程间的通信。
序列化使用场景:
1、持久化存储某个对象。
2、进程间的通信(包括网络中传输对象)。
序列化的实现方式
如果要序列化某个对象,那么这个类应该实现 Serializable 接口或者 Externalizable 接口之一。
实现 Serializable 接口
Serializable 接口是一个空接口,里面没有任何的方法,只要一个对象实现了该接口,那么这个对象就可以被序列化。
序列化步骤:
1、创建 ObjectOutputStream 输出流对象, 指定对象字节流的输出位置。
2、通过 writeObject() 将对象的字节流写入文件。
3、关闭输出流。
反序列化步骤:
1、创建一个 ObjectInputStream 输入流,指定需要读取的文件。
2、通过 readObject() 获取已经序列化的对象。
package javaIO; import java.io.Serializable; //创建一个 Student 类实现 Serializable 接口 public class Student implements Serializable{ private String name; private int num; //只写了带参的构造函数, 没有提供无参的构造函数 public Student(String name, int num) { super(); this.name = name; this.num = num; System.out.println("反序列化是否调用了构造函数?"); } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getNum() { return num; } public void setNum(int num) { this.num = num; } @Override public String toString() { return "Student [name=" + name + ", num=" + num + "]"; } }
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class TestSerializable { public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException { // 创建 ObjectOutputStream 输出流对象, 传入一个 FileOutputStream 对象, 指定对象字节流的输出位置 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\javatest\\student.txt")); Student student = new Student("Zz-feng", 1001); //通过 writeObject() 将对象的字节流写入文件 out.writeObject(student); //关闭输出流 out.close(); // 创建 ObjectOutputStream 输出流对象, 传入一个 FileInputStream 对象, 指定需要反序列化的文件 ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\javatest\\student.txt")); //通过 readObject() 将以序列化的对象恢复 Object object = in.readObject(); System.out.println(object); //输出 Student [name=Zz-feng, num=1001] //关闭输入流 in.close(); } }
通过上面的例子我们看到,在反序列的过程中,并没有调用类的构造函数。反序列化的对象是由 JVM 自己生成的,不需要调用构造方法。
成员变量是引用的序列化
如果一个可序列化的类的成员不是基本类型,也不是String类型,而是一个引用类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。
public class Person{ //省略相关属性与方法 } public class Teacher implements Serializable { private String name; private Person person; public Teacher(String name, Person person) { this.name = name; this.person = person; } public static void main(String[] args) throws Exception { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) { Person person = new Person("路飞", 20); Teacher teacher = new Teacher("雷利", person); oos.writeObject(teacher); } } }
我们看到程序直接报错,因为Person类的对象是不可序列化的,这导致了Teacher的对象不可序列化。
类的静态变量不会被序列化。
同一对象序列化多次
同一对象序列化多次,会将这个对象序列化多次吗?答案是否定的。
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class TestSerializable { public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException { // 创建 ObjectOutputStream 输出流对象, 传入一个 FileOutputStream 对象, 指定对象字节流的输出位置 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\javatest\\student.txt")); Student s1 = new Student("Zz-feng", "1001"); Student s2 = new Student("mytest", "1002"); Student s3 = new Student("mytest", "1002"); //通过 writeObject() 将对象的字节流写入文件 out.writeObject(s1); out.writeObject(s2); out.writeObject(s3); out.writeObject(s1); out.writeObject(s2); out.writeObject(s3); //关闭输出流 out.close(); // 创建 ObjectOutputStream 输出流对象, 传入一个 FileInputStream 对象, 指定需要反序列化的文件 ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\javatest\\student.txt")); //通过 readObject() 将以序列化的对象恢复 Object read_s1 = in.readObject(); Object read_s2 = in.readObject(); Object read_s3 = in.readObject(); Object read_s11 = in.readObject(); Object read_s22 = in.readObject(); Object read_s33 = in.readObject(); System.out.println(read_s1 == read_s2); // false System.out.println(read_s2 == read_s3); // false System.out.println(read_s1 == read_s33); // false System.out.println(read_s2 == read_s33); // false System.out.println(read_s1 == read_s11); // true System.out.println(read_s2 == read_s22); // true //关闭输入流 in.close(); } }
从输出结果来看,同一个对象只会序列化一次,并不是每次调用 writeObject() 方法都会序列化一个对象。
序列化过程
1、如果一个对象已经被序列化过,那么这个对象会保存一个序列化编码号,并将序列化编号一起保存在输出的序列化文件中。
2、当程序试图序列化一个对象时,首先会检查该对象是否已经存在序列化编号,如果有,则直接输出编号到序列化文件中。否则进行序列化。
序列化存在的问题
根据序列的过程,如果一个对象已经被序列化了,那么再次调用 writeObject() 方法并不会再次序列化该对象。如果该对象的属性是可以 set() 等方式修改的,那么反序列化得到的对象并不会显示被修改的内容。
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class TestSerializable { public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException { // 创建 ObjectOutputStream 输出流对象, 传入一个 FileOutputStream 对象, 指定对象字节流的输出位置 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\javatest\\student.txt")); Student s1 = new Student("Zz-feng", "1001"); Student s2 = new Student("mytest", "1002"); //通过 writeObject() 将对象的字节流写入文件 out.writeObject(s1); s1.setName("newName"); s1.setNum("newNum"); out.writeObject(s1); out.writeObject(s1); //关闭输出流 out.close(); // 创建 ObjectOutputStream 输出流对象, 传入一个 FileInputStream 对象, 指定需要反序列化的文件 ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\javatest\\student.txt")); //通过 readObject() 将以序列化的对象恢复 Object read_s1 = in.readObject(); Object read_s11 = in.readObject(); System.out.println(read_s1); // Student [name=Zz-feng, num=1001] System.out.println(read_s11); // Student [name=Zz-feng, num=1001] //关闭输入流 in.close(); } }
自定义序列化
有些时候,我们有这样的需求,某些属性不需要序列化。使用 transient 关键字选择不需要序列化的字段。
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class TestSerializable { public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException { // 创建 ObjectOutputStream 输出流对象, 传入一个 FileOutputStream 对象, 指定对象字节流的输出位置 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\javatest\\student.txt")); Student s1 = new Student("Zz-feng", "1001", 18, "香波地岛"); //通过 writeObject() 将对象的字节流写入文件 out.writeObject(s1); out.writeObject(s1); //关闭输出流 out.close(); // 创建 ObjectOutputStream 输出流对象, 传入一个 FileInputStream 对象, 指定需要反序列化的文件 ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\javatest\\student.txt")); //通过 readObject() 将以序列化的对象恢复 Student read_s1 = (Student)in.readObject(); System.out.println(read_s1); // Student [name=Zz-feng, num=1001] System.out.println(read_s1.getAge()); // 0 System.out.println(read_s1.getAddress()); // null //关闭输入流 in.close(); } }
通过上面的代码我们发现了一个问题,尽管 transient 可以决定哪些内容是不需要序列化的,但在反序列化的过程中,JVM会忽视那些没有被序列化的部分,这样我们并不能完整的获得这个对象。对于 transient 修饰的成员变量会赋默认值。
对象的某些信息可能是敏感信息,比如银行卡号等,如果直接序列化可能存在信息泄露的风险,如果使用 transient 不进行序列化,那么反序列化时又得不到这个对象的完整信息。为了解决这些问题,我们可以重写以下三个方法,从而对序列化的信息进行加密,防止信息泄露:
1、private void writeObject(ObjectOutputStream out) //序列化时会调用此方法
2、private void readObject(ObjectInputStream in) //反序列化时会调用此方法
3、private void readObjectNoData() //当序列化的类版本和反序列化的类版本不同时,或者 ObjectInputStream 流被修改时,会调用此方法。
Serializable 接口是一个空接口,上面的三个方法都不是必需的,但一般我们会重写前面两个。
import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; //创建一个 Student 类实现 Serializable 接口 public class Student implements Serializable{ private String name; private String num; private transient int age; private transient String address; //自定义序列化 private void writeObject(ObjectOutputStream out) throws IOException { //只序列化以下3个成员变量 out.writeObject(name); out.writeInt(age); //写入反序后的信息,当然我们也可以使用其他加密方式。这样别人打开文件,看到的就不是真正的信息,更安全。 out.writeObject(new StringBuffer(address).reverse()); } //自定义反序列化。注意:read()的顺序要和write()的顺序一致。比如说序列化时写的顺序是name、age、address,反序列化时读的顺序也要是name、age、address private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { //readObject()返回的是Object,要强制类型转换 this.name= (String)in.readObject(); this.name=(String)in.readObject(); //反序才得到真正的信息 StringBuffer pwd=(StringBuffer)in.readObject(); this.address=pwd.reverse().toString(); } }
public class Test { public static void main(String[] args) throws IOException, ClassNotFoundException { Student s1=new Student ("张三", "1234", 18, "空岛"); //序列化 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("./obj.txt")); //调用我们自定义的writeObject()方法 out.writeObject(s1); out.close(); //反序列化 ObjectInputStream in = new ObjectInputStream(new FileInputStream("./obj.txt")); //调用自定义的readObject()方法 Student s = (Student )in.readObject(); in.close() //测试 System.out.println(s.getAge()); //18 System.out.println(s.getName()); //张三 System.out.println(s.getAddress()); //空岛 } }
除了上面的方法外,我们还可以使用 private/default/protected/public Object writeReplace(){......} 或者 private/default/protected/public Object writeReplace(){......}。这两个方法返回一个 Object 对象,重写该方法,可以做一些格式化处理。
//implements Serializable class User implements Serializable{ private int id; private String name; private String password; public User(int id,String name,String password){ this.id=id; this.name=name; this.password=password; } //...... private Object writeReplace(){ String info="请编号为"+id+"的客户"+name+"到1号柜台办理业务。"; return info; } }
//implements Serializable class User implements Serializable{ private int id; private String name; private String password; //......其他成员变量 public User(int id,String name,String password){ this.id=id; this.name=name; this.password=password; } //........... //用指定的对象替换掉反序列化读取的对象 private Object readResolve(){ String info="请编号为"+id+"的客户"+name+"到1号柜台办理业务。"; return info; } }
writeReplace:在序列化时,会先调用此方法,再调用 writeObject 方法。此方法可将任意对象代替目标序列化对象。
readResolve:在反序列化读取对象后,会自动调用此方法,将读取的对象替换为指定的对象。反序列化出来的对象被立即丢弃。此方法在readeObject后调用。常用来反序列化单例类,保证单例的唯一性。
两者只是作用的时间点不同,可以联合使用。
Externalizable:强制自定义序列化
除了实现 Serializable 接口外,还可以通过 Externalizable 接口实现序列化。该接口中有两个方法:
public interface Externalizable extends java.io.Serializable { void writeExternal(ObjectOutput out) throws IOException; //调用writeObject()时,会自动调用此方法来序列化对象 void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; //调用readObject()时,会自动调用此方法来反序列化 }
//implements Externalizable class User implements Externalizable{ private int id; private String name; private String password; //......其他成员变量 //必须要有无参的构造函数 public User(){ } public User(int id,String name,String password){ this.id=id; this.name=name; this.password=password; } public int getId() { return id; } public String getName() { return name; } public String getPassword() { return password; } //自定义序列化 @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeInt(id); out.writeObject(name); out.writeObject(password); } //自定义反序列化。注意读的顺序要和写的顺序一致。 @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { this.id=in.readInt(); this.name=(String)in.readObject(); this.password=(String)in.readObject(); } }
Externalizable 接口不同于 Serializable 接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供 pulic 的无参构造器,因为在反序列化的时候需要反射创建对象。
两种序列化对比
实现Serializable接口 | 实现Externalizable接口 |
---|---|
系统自动存储必要的信息,有没有无参构造函数都行 | 程序员决定存储哪些信息,不需要有无参构造函数 |
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持 | 必须实现接口内的两个方法 |
性能略差 | 性能略好 |
虽然Externalizable接口带来了一定的性能提升,但变成复杂度也提高了,所以一般通过实现Serializable接口进行序列化。
版本控制——serialVersionUID
在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。
一个类实现了 Serializable 接口,如果我们没有显示的指定版本号,那么 JVM 会给这个类指定一个默认的 serialVersionUID,默认样式为:
private static final long serialVersionUID = 1L;
当然,我们也可以显示的指定版本号:
private static final long serialVersionUID = 123456789L;
serialVersionUID 主要是为了解决版本问题,如果一个类的方法或字段有所改动,当我们设置了 serialVersionUID 后,在反序列化是就会报出异常。如果我们没有设置serialVersionUID,由于默认的 serialVersionUID 的值是一样的,所以在反序列化过程中,缺少的字段会默认空值,多余的字段会被丢弃。
使用 serialVersionUID 的情形:
1、如果只是修改了方法,反序列化不容影响,则无需修改版本号;
2、如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;
3、如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。