概述
Java 对象的序列化和反序列化, 这个词对我来说追溯到大学阶段, 学 Java 对象流时知道有这东西. 老师告诉我们可以把 Java 对象化作字节流, 储存文件或网络通信. 然后就是巴啦巴拉, 一脸懵逼. 举个例子, 有一台北京的 Java 虚拟机现在运行的某个对象要调用一台在长春运行的 Java 虚拟机内的某个对象, 这是两个不同的 Java 虚拟机进程, 我们没办法直接传递对象的引用, 现在我们只能把长春的这个对象序列化, 变成一块一块碎片, 传给北京的虚拟机, 北京虚拟机反序列化后就造出了一个对象, 然后就可以正常使用. 说得通俗点, 这个序列化就是跨进程数据传输.
序列化(Serializable 接口)
要序列化的类通过实现 java.io.Serializable 接口启动序列化的功能, 如果它有子类, 所有的子类本身也都可序列化.
Person 类
- public class Person implements Serializable
- {
- private String name;
- private int age;
- private String sex;
- public Person(String name,int age,String sex)
- {
- this.name = name;
- this.age = age;
- this.sex = sex;
- }
- public int getAge()
- {
- return age;
- }
- public String getName()
- {
- return name;
- }
- public String getSex()
- {
- return sex;
- }
- @Override
- public String toString()
- {
- return "姓名:" + this.name + ", 年龄:" + this.age + ", 性别:" + this.sex;
- }
- }
serializable 接口没有函数或者字段, 我们可以看到我们 implements 接口, 没实现任何的函数, 它仅仅用于标识可序列化, 如果我们没有实现这个标识接口而进行序列化, 会抛出一个 NotSerializableException 异常.
Main 类
- public class Main
- {
- public static void main(String[] args)
- {
- serializePerson();
- }
- private static void serializePerson()
- {
- try {
- Person customer = new Person("张三",15,"男");
- ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:/Person"));
- objectOutputStream.writeObject(customer);
- System.out.println("Person 序列化完成...");
- objectOutputStream.close();
- }catch (Exception e){
- e.printStackTrace();
- System.out.println("Person 序列化出错...");
- }
- }
- }
输出
Person 序列化完成..
在 E 盘下就会有一个 Person 文件, 用 notepad++ 打开, 依稀可以见到一些熟悉的字眼
我们用二进制查看器打开这个文件
左边第一个部分是序列化的文件头 AC ED 00 05, 其他还有关于序列化的类描述, 里面的各个属性值, 还有父类的信息, lz 实在看不懂了, 有大佬分析过序列化文件, 有兴趣可自行百度查看.
反序列化
Main 类添加 DeserializePerson 函数
- private static Object DeserializePerson()
- {
- ObjectInputStream objectInputStream = null;
- try {
- objectInputStream = new ObjectInputStream(new FileInputStream(new File("E:/Person")));
- Object object = objectInputStream.readObject();
- System.out.println("反序列化完成...");
- return object;
- } catch (Exception e)
- {
- e.printStackTrace();
- }finally
- {
- if (objectInputStream != null )
- {
- try
- {
- objectInputStream.close();
- } catch (IOException e)
- {
- e.printStackTrace();
- }
- }
- }
- return null;
- }
输出
反序列化完成...
姓名: 张三, 年龄: 15, 性别: 男
serialVersionUID(标识)
知道 serializable 是标识的语义, 这个标识是在哪? 如果我们没特意指定, 在编译过程中 Java 编译器会默认赋予它一个独一无二的编号, 保证它是唯一的. 但这样做是否会给我们带来影响?
Person 类
- public class Person implements Serializable
- {
- private String name;
- private int age;
- private String sex;
- // 添加了一个属性
- private String number;
- public Person(String name,int age,String sex)
- {
- this.name = name;
- this.age = age;
- this.sex = sex;
- }
- public int getAge()
- {
- return age;
- }
- public String getName()
- {
- return name;
- }
- public String getSex()
- {
- return sex;
- }
- @Override
- public String toString()
- {
- return "姓名:" + this.name + ", 年龄:" + this.age + ", 性别:" + this.sex;
- }
- }
Main 类
- public class Main
- {
- public static void main(String[] args)
- {
- //serializePerson();
- Person person = (Person) DeserializePerson();
- System.out.println(person);
- }
- private static Object DeserializePerson()
- {
- ObjectInputStream objectInputStream = null;
- try {
- objectInputStream = new ObjectInputStream(new FileInputStream(new File("E:/Person")));
- Object object = objectInputStream.readObject();
- System.out.println("反序列化完成...");
- return object;
- } catch (Exception e)
- {
- e.printStackTrace();
- }finally
- {
- if (objectInputStream != null )
- {
- try
- {
- objectInputStream.close();
- } catch (IOException e)
- {
- e.printStackTrace();
- }
- }
- }
- return null;
- }
- private static void serializePerson()
- {
- try {
- Person customer = new Person("张三",15,"男");
- ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:/Person"));
- objectOutputStream.writeObject(customer);
- System.out.println("Person 序列化完成...");
- objectOutputStream.close();
- }catch (Exception e){
- e.printStackTrace();
- System.out.println("Person 序列化出错...");
- }
- }
- }
发现抛出了一个异常
本地的文件流中的 class(序列化)和修改完的 Person.class, 不兼容了(UID), 处于安全机制考虑, 程序抛出错误, 拒绝载入. 如何保证 UID 版本一致, 那只能自己指定 UID, 在序列化后, 去添加字段或者函数, 就不会影响后期还原.
Person 类
- public class Person implements Serializable
- {
- private static final long serialVersionUID = 55555L;
- private String name;
- private int age;
- private String sex;
- public Person(String name,int age,String sex)
- {
- this.name = name;
- this.age = age;
- this.sex = sex;
- }
- public int getAge()
- {
- return age;
- }
- public String getName()
- {
- return name;
- }
- public String getSex()
- {
- return sex;
- }
- @Override
- public String toString()
- {
- return "姓名:" + this.name + ", 年龄:" + this.age + ", 性别:" + this.sex;
- }
- }
Main 类
- public class Main
- {
- public static void main(String[] args)
- {
- serializePerson();
- Person person = (Person) DeserializePerson();
- System.out.println(person);
- }
- private static Object DeserializePerson()
- {
- ObjectInputStream objectInputStream = null;
- try {
- objectInputStream = new ObjectInputStream(new FileInputStream(new File("E:/Person")));
- Object object = objectInputStream.readObject();
- System.out.println("反序列化完成...");
- return object;
- } catch (Exception e)
- {
- e.printStackTrace();
- }finally
- {
- if (objectInputStream != null )
- {
- try
- {
- objectInputStream.close();
- } catch (IOException e)
- {
- e.printStackTrace();
- }
- }
- }
- return null;
- }
- private static void serializePerson()
- {
- try {
- Person customer = new Person("张三",15,"男");
- ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:/Person"));
- objectOutputStream.writeObject(customer);
- System.out.println("Person 序列化完成...");
- objectOutputStream.close();
- }catch (Exception e){
- e.printStackTrace();
- System.out.println("Person 序列化出错...");
- }
- }
- }
序列化后, 我们注释掉 main 函数里的 serializePerson(); 修改 Person 类, 添加或修改字段, 进行反序列化.
反序列化完成...
姓名: 张三, 年龄: 15, 性别: 男
我们可以发现, 由编译器默认自动给我们生成的 UID 编码, 并不可控, 对同一个类, A 编译器编译, 赋予一个 UID 的值和 B 编译器编译赋予的 UID 值也有可能不同, 所以为了提高可控性, 确定性, 我们在一个可序列化的类中应该明确为它赋值.
Externalizable 接口
以上我们可以发现, 所有的序列化操作都是默认的, 自动帮我们完成. 但有时我们并不想这样, 有些属性我们并不想序列化, 想要自定义的方式去序列化它. 为此, Java 提供了一个 Externalizable 接口, 方便用户自定义序列化过程, 它和 Serializable 有什么区别?
Person 类
- public class Person implements Externalizable
- {
- private static final long serialVersionUID = 55555L;
- private String name;
- private int age;
- private String sex;
- public Person()
- {
- }
- public Person(String name,int age,String sex)
- {
- this.name = name;
- this.age = age;
- this.sex = sex;
- }
- public int getAge()
- {
- return age;
- }
- public String getName()
- {
- return name;
- }
- public String getSex()
- {
- return sex;
- }
- @Override
- public String toString()
- {
- return "姓名:" + this.name + ", 年龄:" + this.age + ", 性别:" + this.sex;
- }
- @Override
- public void writeExternal(ObjectOutput out) throws IOException
- {
- }
- @Override
- public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
- {
- }
- }
Main 类不变, 输出
Person 序列化完成...
反序列化完成...
姓名: null, 年龄: 0, 性别: null
与 serialization 接口相比, 我们很快就能看出, Externalizable 接口对 Person 类进行序列化和反序列化之后得到的对象的状态并没有保存下来, 所有属性的值都变成默认值. 它们之间有什么关系和区别?
Externalizable 继承了 Serializable, 它定义了两个抽象函数, writeExternal 和 readExternal, 我们进行序列化和反序列需要重写, 可以指定序列化哪些属性.
Externalizable 序列化的类必须有一个无参构造函数, 否则会报错. 因为 Externalizable 序列化的时候, 读取对象时, 会调用无参构造函数创建一个新的对象, 之后将保存对象的字段的值填充到新对象中.
修改 Person 类, 重新序列化
- @Override
- public void writeExternal(ObjectOutput out) throws IOException
- {
- out.writeObject(name);
- out.writeObject(age);
- out.writeObject(sex);
- }
- @Override
- public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
- {
- this.name = (String)in.readObject();
- this.age = (int)in.readObject();
- this.sex = (String)in.readObject();
- }
输出
Person 序列化完成...
反序列化完成...
姓名: 张三, 年龄: 15, 性别: 男
重写完两个函数, 发现对象持久化完成. 但细心的小伙伴可能会发现, 我们序列化的成员变量都是实例变量. 就会有一个疑问, 换成静态变量试试?
静态变量被序列化?
其实序列化 (默认序列化) 被不保存静态变量, 因为静态变量属于类本身, 对象序列化, 顾名思义就是指的对象本身状态, 并不包含静态变量.
- public class Person implements Serializable
- {
- private static final long serialVersionUID = 55555L;
- private String name;
- private int age;
- private String sex;
- private static String money;
- public Person(String name,int age,String sex,String money)
- {
- this.name = name;
- this.age = age;
- this.sex = sex;
- this.money = money;
- }
- public int getAge()
- {
- return age;
- }
- public String getName()
- {
- return name;
- }
- public String getSex()
- {
- return sex;
- }
- @Override
- public String toString()
- {
- return "姓名:" + this.name + ", 年龄:" + this.age + ", 性别:" + this.sex + ", 资产:" + money;
- }
- }
Main 类
- public class Main {
- public static void main(String[] args) throws Exception {
- serializablePerson();
- Person person = (Person)DeserializablePerson();
- System.out.println(person);
- }
- // 演示使用, 并不规范
- private static void serializablePerson() throws Exception {
- Person person = new Person("张三",15,"男","5000000");
- ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:/Person"));
- objectOutputStream.writeObject(person);
- objectOutputStream.close();
- System.out.println("序列化完成...");
- }
- private static Object DeserializablePerson() throws Exception
- {
- ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("D:/Person")));
- Object object = objectInputStream.readObject();
- objectInputStream.close();
- System.out.println("反序列完成...");
- return object;
- }
- }
输出
序列化完成...
反序列完成...
姓名: 张三, 年龄: 15, 性别: 男, 资产: 5000000
结果跟我们的结论出乎意料, 静态变量被序列化了, 真的是这样吗? 导致这个原因是因为我们测试都是在一个进程里面的. JVM 把 money 这个变量加载进来了, 所以导致我们看到的是加载过的 money. 我们可以这样做, 多写一个 Main 类, 让 JVM 退出后, 重新加载.
MainTest 类
- public class MainTest {
- public static void main(String[] args) throws Exception {
- Person person = (Person)DeserializablePerson();
- System.out.println(person);
- }
- private static Object DeserializablePerson() throws Exception
- {
- ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("D:/Person")));
- Object object = objectInputStream.readObject();
- objectInputStream.close();
- System.out.println("反序列化完成...");
- return object;
- }
- }
现在先运行 Main 类, 得到的是我们上面的接口, 现在运行 MainTest 类
反序列化完成...
姓名: 张三, 年龄: 15, 性别: 男, 资产: null
可以发现静态成员变量并没有被保存下来, 变成一个默认值.
transient 关键字(默认序列化)
有时候我们并不想自定义序列化, 然而有些成员变量我们也不想序列化. 那么 transient 这个关键字就是你的不二人选, 它的作用很简单, 就是控制变量的序列化, 在变量声明前加上这个关键字即可.
Person 类
- public class Person implements Serializable
- {
- private static final long serialVersionUID = 55555L;
- private String name;
- private int age;
- private String sex;
- private static String money;
- // 银行账户
- private transient String bankNumber;
- // 银行密码
- private transient int passWord;
- public Person(String name,int age,String sex,String money,String bankNumber,int passWord)
- {
- this.name = name;
- this.age = age;
- this.sex = sex;
- this.money = money;
- this.bankNumber = bankNumber;
- this.passWord = passWord;
- }
- public int getAge()
- {
- return age;
- }
- public String getName()
- {
- return name;
- }
- public String getSex()
- {
- return sex;
- }
- @Override
- public String toString()
- {
- return "姓名:" + this.name + ", 年龄:" + this.age + ", 性别:" + this.sex + ", 资产:" + money + ", 我的银行账户是:" + this.bankNumber + ", 我的银行密码:" + this.passWord;
- }
- }
Main 类
- public class Main {
- public static void main(String[] args) throws Exception {
- serializablePerson();
- Person person = (Person)DeserializablePerson();
- System.out.println(person);
- }
- // 演示使用, 并不规范
- private static void serializablePerson() throws Exception {
- Person person = new Person("张三",15,"男","5000000","564654979797464646",123456);
- ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:/Person"));
- objectOutputStream.writeObject(person);
- objectOutputStream.close();
- System.out.println("序列化完成...");
- }
- private static Object DeserializablePerson() throws Exception
- {
- ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("D:/Person")));
- Object object = objectInputStream.readObject();
- objectInputStream.close();
- System.out.println("反序列完成...");
- return object;
- }
- }
输出
序列化完成...
反序列完成...
姓名: 张三, 年龄: 15, 性别: 男, 资产: 5000000, 我的银行账户是: null, 我的银行密码: 0
这些关键信息就都不会被序列化到文件中, 当然我有 500w 的话
序列化的存储规则
Java 序列化为了节省存储空间, 有特定的存储规则, 写入文件为同一对象的时候, 并不会再将对象的内容存储, 而只是再次存储一份引用.
Main 类
- public class Main {
- public static void main(String[] args) throws Exception {
- serializablePerson();
- Person person = (Person) DeserializablePerson();
- System.out.println(person);
- }
- // 演示使用, 并不规范
- private static void serializablePerson() throws Exception {
- Person person = new Person("张三",15,"男","5000000","564654979797464646",123456);
- ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:/Person"));
- objectOutputStream.writeObject(person);
- objectOutputStream.flush();
- System.out.println(new File("D:/Person").length());
- objectOutputStream.writeObject(person);
- objectOutputStream.close();
- System.out.println(new File("D:/Person").length());
- System.out.println("序列化完成...");
- }
- private static Object DeserializablePerson() throws Exception
- {
- ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("D:/Person")));
- Person person =(Person) objectInputStream.readObject();
- Person person1 =(Person) objectInputStream.readObject();
- objectInputStream.close();
- System.out.println("反序列完成...");
- System.out.print("是否同一个对象 =====>");
- System.out.println(person == person1);
- return person;
- }
- }
输出
108
113
序列化完成...
反序列完成...
是否同一个对象 =====>true
姓名: 张三, 年龄: 15, 性别: 男, 资产: 5000000, 我的银行账户是: null, 我的银行密码: 0
多出五字节存储空间就是新增引用和一些控制信息空间, 反序列时, 恢复引用关系, person 和 person1 都指向唯一的对象, 二者相等, 输出 true, 这样的存储规则就极大节省了存储的空间.
注意事项
可以发现, 我很多地方加了默认序列化的情况下, 如果是自定义序列化, 那么 transient 这些就统统无效, 是不是感觉可控性增强不少, 序列化还得注意几个点.
如果有内部类, 或者是要序列化的对象的成员变量是一个对象类, 那么也必须继承序列化的接口, 否则会出错滴.
子类即使没有实现序列化的接口, 只要父类实现了, 那子类就可以直接序列化.
参考: Java 序列化的高级认识
===============================================================
如发现错误, 请及时留言, lz 及时修改, 避免误导后来者. 感谢!!!
来源: https://www.cnblogs.com/dslx/p/10648414.html