【Java基础】序列化与反序列化

文章参考 https://blog.csdn.net/shusheng0007/article/details/80629348

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

  • 序列化: 把Java对象转换为字节序列(其实就是字节数组)的过程。
  • 反序列化:把字节序列(字节数组)恢复为Java对象的过程。

2. 什么要进行序列化和反序列化

在网络上传输各种类型的数据,包括文本、图片、音频、视频等, 这些数据不可能在传输过程中还是以他们原来的样子传输,而是变成1000111100101这样的二进制表示的一个个二进制的字节序列,那么发送方就需要将这些数据序列化为字节流后传输,而接收方接到字节流后需要反序列化为相应的数据类型。当然接收方也可以将接收到的字节流存储到磁盘中,等到以后想恢复的时候再恢复。

综上,可以得出对象的序列化和反序列化主要有两种用途:

  • 把对象的字节序列永久地保存到磁盘上。(持久化对象)
  • 可以将Java对象以字节序列的方式在网络中传输。(网络传输对象)

3. 只能使用序列化吗

  • 在磁盘文件中,不能去保存和恢复对象的内存地址是因为对象被重载时,它可能占据的是与原来完全不同的内存地址。
  • 在网络传输中,不同的处理器之间通信时,对象占据的内存地址也是完全不同。

因此是的,持久化和网络传输只能使用序列化。

4. 序列化的方式

  • JDK库中的序列化API实现二进制序列化
  • XML
  • JSON
  • Protostaff

1. JDK序列化

1. 对象序列化包括如下步骤:

创建一个对象输出流,它可以包装一个其他类的目标输出流,如文件输出流;
通过对象输出流的writeObject()方法写对象。

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("empoyee.dat"));
Employee harry = new Employee("Harry Hacker", 5000, 1989, 10, 1);
Maneger boss = new Manager("Carl Cracker", 7000, 1984, 12, 15);
oos.writeObject(harry);
oos.writeObject(boss);

2. 对象反序列化包括如下步骤:

创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
通过对象输入流的readObject()方法以这些对象被写出的顺序读取对象并获得它们。

ObjectInputStream ois = new ObjectInputStream (new FileInputStream("empoyee.dat"));
Employee e1 = (Employee)ois.readObject();
Maneger e2 = (Manager)ois.readObject();

3. 对于那么需要序列化与反序列化的对象,对应的类必须要实现JDK库的相关API,有以下三种方法:

  • 若Club类仅仅实现了Serializable接口,则可以按照以下方式进行序列化和反序列化
    ObjectOutputStream采用默认的序列化方式,对Club对象的非transient的实例变量进行序列化。
    ObjcetInputStream采用默认的反序列化方式,对对Club对象的非transient的实例变量进行反序列化。

  • 若Club类仅仅实现了Serializable接口,并且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),则采用以下方式进行序列化与反序列化。
    ObjectOutputStream调用Student对象的writeObject(ObjectOutputStream out)的方法进行序列化。
    ObjectInputStream会调用Student对象的readObject(ObjectInputStream in)的方法进行反序列化。

  • 若Club类实现了Externalnalizable接口,且Club类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,则按照以下方式进行序列化与反序列化。
    ObjectOutputStream调用Student对象的writeExternal(ObjectOutput out))的方法进行序列化。
    ObjectInputStream会调用Student对象的readExternal(ObjectInput in)的方法进行反序列化。

1. 实现Serializable接口

如果是对序列化的需求非常简单,没有对序列化过程控制的需求,可以简单实现Serializable接口即可。
从Serializable的源码可知,其是一个标记接口,无需实现任何方法。例如我们有如下的Student类.

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

        public Student(String name,int age)
        {
            System.out.println("有参数构造器执行");
            this.name=name;
            this.age=age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }

序列化: 那么我们如何将此类的对象序列化后保存到磁盘上呢?

  • 创建一个 ObjectOutputStream 输出流oos
  • 调用此输出流oos的writeObject()方法
private static void serializ()
{
    try (ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("object.txt"));)
    {
       Student s=new Student("ben",18);
       oos.writeObject(s);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

上面代码将Sutent 的一个实例对象序列化到了一个文本文件中。

反序列化:我们如从文本文件中将此对象的字节序列恢复成Student对象呢?

  • 创建一个ObjectInputStream 输入流ois
  • 调用此输入流ois的readObject()方法。
 private static void deSerializ()
 {
     try(ObjectInputStream ois=new ObjectInputStream(new FileInputStream("object.txt"));)
     {
         Student s= (Student) ois.readObject();
         System.out.println(s.toString());
     }catch (Exception e)
     {
         e.printStackTrace();
     }
 }

Note: 当反序列化的时候并没有调用Student的构造函数,说明反序列化机制无需通过构造器来构建Java对象,这就给实现了序列化机制的单例模式造成了麻烦。

2. 版本 serialVersionUID

由于反序列化Java对象的时候,必须提供该对象的class文件,但是随着项目的升级class文件文件也会升级,Java如何保证兼容性呢?答案就是 serialVersionUID。每个可以序列化的类里面都会存在一个serialVersionUID,只要这个值前后一致,即使类升级了,系统仍然会认为他们是同一版本。如果我们不显式指定一个,系统就会使用默认值。

public class Student implements Serializable {
    private static final long serialVersionUID=1L;
    ...
}

我们应该总是显式指定一个版本号,这样做的话我们不仅可以增强对序列化版本的控制,而且也提高了代码的可移植性。因为不同的JVM有可能使用不同的策略来计算这个版本号,那样的话同一个类在不同的JVM下也会认为是不同的版本。

那么我们如何维护这个版本号呢?

  • 只修改了类的方法,无需改变serialVersionUID;
  • 只修改了类的static变量和使用transient 修饰的实例变量,无需改变serialVersionUID;
  • 如果修改了实例变量的类型,例如一个变量原来是int改成了String,则反序列化会失败,需要修改serialVersionUID;如果删除了类的一些实例变量,可以兼容无需修改;如果给类增加了一些实例变量,可以兼容无需修改,只是反序列化后这些多出来的变量的值都是默认值。

3. 继承及引用对象序列化

当要序列化的类存在父类的时候,直接或者间接福来,其父类也必须可以序列化。

当要序列化的类中引用了其他类的对象,那么这些对象的类也必须是可序列化的,如下面代码中的Teacher 类也必须是可以序列化的

public class Student implements Serializable {    
    private Teacher teacher;
    ...
}

4. Java序列化算法

Java序列化遵循以下算法:

  • 所有序列化过的,包括磁盘中的的实例对象都有一个序列化编号;
  • 当试图序列化一个对象时,程序会先检查该对象是否已经被序列化过,当对象在本次虚拟机中从未被序列化过,则系统将其序列化为字节序列并输出;
  • 如果某个对象在本次虚拟机中已经序列化过,则直接输出这个序列化编号.

鉴于以上的算法可能会造成一个潜在的问题:当序列化一个可变对象时,只有第一次使用writeObject()方法输出时才会输出字节序列,而第二次调用时仅仅输出一个序列化编号,即使我们改变了这个对象的一些属性,这些改变后的属性也不会序列化到磁盘上,这点在开发中需要非常注意。下面我们看一下代码:

private static void reSerialize()
{
    try(ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("student.txt"));
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream("student.txt"));)
    {
        Student s=new Student("ben",18);
        oos.writeObject(s);
        Student rs1= (Student) ois.readObject();

        s.setAge(32);
        oos.writeObject(s);
        Student rs2= (Student) ois.readObject();

        System.out.println("两个对象是否相等:"+ (rs1==rs2));
        System.out.println("希望年龄变为32:"+rs2.getAge());
    }catch (Exception e)
    {
        e.printStackTrace();
    }
}

输出结果:

两个对象是否相等:true
希望年龄变为32:18

从输出结果可以看出,修改前后反序列化出来的两个对象时绝对相等的,输出的其实是第一个对象,而且我们对年龄做的修改也没有生效。

5. 自定义序列化

  • 通过tansient阻止实例变量的序列化。

    Java默认会序列化所有的实例变量,如果我们不想序列化某一个实例变量,就可以使用tansient这个关键字修饰。

    private transient String name;
  • 通过writeObject()与readObject()方法控制序列化过程
    只需要为实现了Serializable接口的类提供两个如下签名的方法,就可完全控制序列化和发序列化过程。

     private void writeObject(ObjectOutputStream out) throws IOException
     private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException

    例如我们给前面介绍的Student类添加两个如下方法。

     private void writeObject(ObjectOutputStream out) throws IOException
     {
         out.writeObject("hello "+name);
         out.writeInt(age);
     }
    
     private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException
     {
         name= (String) in.readObject();
         age=in.readInt();
     }

    那么反序列化后name 属性的值就会加上hello 前缀。

  • 通过writeReplace()方法控制序列化过程

    为实现了Serializable接口的类提供 如下签名的方法

    Any-Access-Modifier Object writeReplace() throws ObjectStreamException

    该方法在开始序列化writeObject()之前执行,所以可以在序列化对象之前对要序列化的对象做一些处理,甚至完全替换掉原来的对象。 例如下面的代码无论被序列化的对象是什么,反序列化出来的对象总是一个字符串“总有刁民想害朕”。

    private Object writeReplace() throws ObjectStreamException{
        return "总有刁民想害朕";
    }
  • 通过readResolve()方法控制反序列化过程

    为实现了Serializable接口的类提供 如下签名的方法

    Any-Access-Modifier Object readResolve() throws ObjectStreamException

    该方法在反序列化readObject()后执行,所以可以在反序列化后对获得的对象做一些处理,甚至完全替换为其他对象。例如下面代码无论反序列化后得到的对象是什么,都会被替换成一个字符串”昏君人人得而诛之”。

     private Object readResolve() throws ObjectStreamException{
          return "昏君人人得而诛之";
     }

    这个函数在单例类实现序列化时特别有用,通过前面的介绍 我们知道,通过序列化可以不使用构造函数而获取一个类的实例,这样的话一个单例类就会存在两个实例了,就失去效用了。那么如何解决这个问题呢?

    1、最好是使用枚举enum来构建一个单例,这是最好的方法,解决了序列化以及反射生成实例的问题。

    public enum Singleton {
      INSTANCE;
    }

    2、如果只是解决由于序列化导致的单例破坏问题,可以使用readResolve()方法解决,如下代码所示:

    public class Singleton implements Serializable{
      public static final Singleton INSTANCE = new Singleton();
      private Singleton() {
      }
      protected Object readResolve() {
          return INSTANCE;
      }
      ...
    }

6. 实现Externalizable接口

程序员需要自己实现序列化,一般不用。

2. 其他序列化方式

可以参考https://www.cnblogs.com/BigJunOba/p/9127414.html

全部评论

相关推荐

11-14 16:13
已编辑
重庆科技大学 测试工程师
Amazarashi66:不进帖子我都知道🐮❤️网什么含金量
点赞 评论 收藏
分享
11-18 15:57
门头沟学院 Java
最终归宿是测开:这个重邮的大佬在重邮很有名的,他就喜欢打92的脸,越有人质疑他,他越觉得爽😂
点赞 评论 收藏
分享
不愿透露姓名的神秘牛友
11-24 20:55
阿里国际 Java工程师 2.7k*16.0
程序员猪皮:没有超过3k的,不太好选。春招再看看
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务