Java序列化实战

234 阅读5分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

1.什么是java序列化和反序列化

序列化:将java对象转换为字节序列

反序列化:将字节序列转换为java对象

为什么要做序列化:序列化对象,可以使得对象可以保存在磁盘中或者可以通过网络进行传输,对象不必依赖程序而存在,反序列化对象可以将字节序列转换成原来的对象。

2.序列化方式实现

  • Serializable接口

如果你想要将对象保存到文件中或者通过网络传输,那么该对象的类就要实现Serializable接口

实践:

创建一个java类Student:实现了Serializable接口,

public class Student implements Serializable {
    private String name;
    private int age;
    //省略getter setter 构造函数
    //......
}

创建一个main方法,在main方法中测试。

首先创建一个Student的实例,创建ObjectOutputStream输出流,然后调用ObjectOutputStream的writeObject()方法将studen对象序列化到test.txt文件中。

public static void main(String[] args) {
        try {
            Student student = new Student("张三",18);
            //创建一个输对象出流
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.txt"));
            outputStream.writeObject(student);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

执行,在src文件夹下生成了test.txt文件

我们再反序列化一下看看能不能反序列化回来。

 ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("test.txt"));
Student student2 = (Student) inputStream.readObject();

System.out.println("反序列化:"+student2);

image-20220124171958424.png

从结果可以看出是可以序列化是成功了。

有一点注意的是,反序列化时不会调用构造方法。

属性为引用数据类型的对象,如果要序列化,那么该属性也要实现Serializable接口才能实现该对象的完全序列化,否则就会报错NotSerializableException

新增一个Class(班级)类:

public class Class {
    private String className;
    private int nums;
}

Student新增Calss属性

public class Student implements Serializable {
    private String name;
    private int age;
    private Class aClass;

image-20220124220613878.png

Class实现Serializable接口,调用序列化和反序列化

image-20220124221119124.png

  • java序列化算法
  1. 所有保存到磁盘的对象都有一个序列化编号
  2. 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
  3. 如果此对象已经序列化过,则直接输出编号即可。
  • 序列化存在的问题

由于序列化时不会重复序列化同一个对象,所以对象第一次被序列化之后,后面对对象进行了修改再次序列化时得到的

还是原来的序列化对象。

public static void main(String[] args) {
        try {
            String name=new String("sss");
            Class aClass = new Class("1班",40);
            Student student = new Student("张三",18,aClass);
           
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.txt"));

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("test.txt"));

            outputStream.writeObject(student);
            Student student1 = (Student) inputStream.readObject();
            System.out.println("反序列化:"+student1);
            
            aClass.setClassName("2班");
            student.setaClass(aClass);
            outputStream.writeObject(student);
            Student student2 = (Student) inputStream.readObject();

            System.out.println("反序列化:"+student2);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

输出结果:

image-20220124223452451.png

可以看出我修改后再序列化,反序列化回来的对象还是和第一次一样。

  • transient关键字

如果我们希望有些类的成员不被序列化,可以使用transient关键字修饰。

public class Student implements Serializable {
    private transient String name;
    private int age;
    private Class aClass;
}

输出结果:

反序列化:Student{name='null', age=18, aClass=Class{className='1班', nums=40}}

使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。

此外,你可以通过重新writeObject()和readObject()方法实现自己的序列化方法。

  • 序列化版本号serialVersionUID

随着项目的升级,java序列化的class文件会改变,那么序列化怎么保证前后兼容性呢?java序列化提供了一个private static final long serialVersionUID 的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。

我们先给Student一个序列化版本号,然后把Student序列化保存到stu.txt文件中。

public class Student implements Serializable {
    private transient String name;
    private int age;
    private Class aClass;
}

然后修改,把age的类型改成long;

public class Student implements Serializable {
    private static final long serialVersionUID = 1001L;
    private transient String name;
    private long age;
    private Class aClass;
}

输出结果:

java.io.InvalidClassException: 更文挑战.day01.serializable.Student; local class incompatible: stream classdesc serialVersionUID = 8678683859078700039, local class serialVersionUID = -3533377131204334585
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
	at 更文挑战.day01.serializable.Main.main(Main.java:23)

Student加上序列号版本号

输出结果:

反序列化:Student{name='null', age=18, aClass=Class{className='1班', nums=40}}

虽然我改变了类,但是因为我定义的序列号版本号相同,所以还是反序列化成功,由于我比较好奇如果类型不兼容会怎样,于是我定义了序列化版本号,先序列化保存到文件,修改Student类的age类型为String。

输出反序列化结果:

java.io.InvalidClassException: 更文挑战.day01.serializable.Student; incompatible types for field age
	at java.io.ObjectStreamClass.matchFields(ObjectStreamClass.java:2453)
	at java.io.ObjectStreamClass.getReflector(ObjectStreamClass.java:2347)
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:753)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
	at 更文挑战.day01.serializable.Main.main(Main.java:23)

类型不兼容,反序列化失败。

总结

  1. 需要网络传输的对象的类需要实现Serializable接口。
  2. 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
  3. 使用transient修饰变量不会被序列化。
  4. 序列化对象的引用类型成员变量,也必须是可序列化的,不然会报错。
  5. 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
  6. 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。