Java序列化与反序列化的实例分析讲解

序列化与反序列化

Java对象是有生命周期的,当生命周期结束它就会被回收,但是可以通过将其转换为字节序列永久保存下来或者通过网络传输给另一方。

把对象转换为字节序列的过程称为对象的序列化;把字节序列恢复为对象的过程称为对象的反序列化。

Serializable接口

一个类实现java.io.Serializable接口就可以被序列化或者反序列化。实际上,Serializable接口中没有任何变量和方法,它只是一个标识。如果没有实现这个接口,在序列化或者反序列化时会抛出NotSerializableException异常。

下面是一个实现了Serializable接口的类以及它的序列化与反序列化过程。

public class SerialTest {
  public static void main(String[] args) {
    Test test = new Test();
    test.setName("test");
    // 序列化,存储对象到文本
    ObjectOutputStream oos = null;
    try {
      oos = new ObjectOutputStream(new FileOutputStream("test"));
      oos.writeObject(test);
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      try {
        if (oos != null) {
          oos.close();
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    // 反序列化,从文本中取出对象
    ObjectInputStream ois = null;
    try {
      ois = new ObjectInputStream(new FileInputStream("test"));
      Test1 test1 = (Test1) ois.readObject();
    } catch (IOException e) {
      e.printStackTrace();
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    } finally {
      try {
        if (ois != null) {
          ois.close();
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}
class Test implements Serializable {
  private String name;
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  @Override
  public String toString() {
    return "Test{" +
        "name='" + name + '\'' +
        '}';
  }
}

运行结果:

Test{name='test'}

serialVersionUID

private static final long serialVersionUID = -3297006450515623366L;

serialVersionUID是一个序列化版本号,实现Serializable接口的类都会有一个版本号。如果没有自己定义,那么程序会默认生成一个版本号,这个版本号是Java运行时环境根据类的内部细节自动生成的。最好我们自己定义该版本号,否则当类发生改变时,程序为我们自动生成的序列化版本号也会发生改变,那么再将原来的字节序列反序列化时就会发生错误。

下面是将Test1类加入一个变量age,此时再进行反序列化的结果。可以看出,序列化版本号已发生改变,程序认为不是同一个类,不能进行反序列化。

java.io.InvalidClassException: test.Test1; local class incompatible: stream classdesc serialVersionUID = 9097989105451761251, local class serialVersionUID = -7756223913249050270
 at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:689)
 at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1903)
 at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1772)
 at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2060)
 at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1594)
 at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:430)
 at test.SerialTest.main(SerialTest.java:11)

为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示地定义serialVersionUID,为他赋予明确的值。

那么在IDEA中,怎么手动生成呢?

在settings->Editor->Inspections下,搜索serial,开启Serializable class without 'serialVersionUID'的拼写检查,然后将光标放在实现Serializable的接口上,按住ALt+Enter键,选择添加serialVersionUID即可。

Transient关键字

transient修饰类的变量,可以使变量不被序列化。反序列化时,被transient修饰的变量的值被设为初始值,如int类型被设为0,对象型被设为null。

ObjectOutputStream类和ObjectInputStream类

ObjectOutputStream的writeObject方法可以序列化对象,ObjectInputStream的readObject可以反序列化对象。ObjectOutputStream实现了接口ObjectOutput,所以可以进行对象写操作。ObjectInputStream实现了接口ObjectInput,所以可以对对象进行读操作。

静态变量序列化

给Test类中增加一个静态变量,赋值为12,然后在序列化之后修改其值为10,反序列化之后打印它的值。发现打印的值为10,之前的12并没有被保存。

静态变量是不参与序列化的,序列化只是用来保存对象的状态,而静态变量属于类的状态。

父类序列化

让Test继承一个没有实现Serializable接口的类,设置父类中变量的值,对Test类的实例进行序列化与反序列化操作。

public class SerialTest {
  public static void main(String[] args) {
    Test test = new Test();
    test.setName("huihui");
    test.setSex(12);
    // 序列化,存储对象到文本
    ObjectOutputStream oos = null;
    try {
      oos = new ObjectOutputStream(new FileOutputStream("test"));
      oos.writeObject(test);
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      try {
        if (oos != null) {
          oos.close();
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    // 反序列化,从文本中取出对象
    ObjectInputStream ois = null;
    try {
      ois = new ObjectInputStream(new FileInputStream("test"));
      Test test1 = (Test) ois.readObject();
      System.out.println(test1);
    } catch (IOException | ClassNotFoundException e) {
      e.printStackTrace();
    } finally {
      try {
        if (ois != null) {
          ois.close();
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}
class Test extends TestFather implements Serializable {
  private static final long serialVersionUID = 4335715933640891747L;
  private String name;
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  @Override
  public String toString() {
    return "Test{" +
        "name='" + name + '\'' +
        "sex='" + sex + '\'' +
        '}';
  }
}
class TestFather {
  protected Integer sex;
  public Integer getSex() {
    return sex;
  }
  public void setSex(Integer sex) {
    this.sex = sex;
  }
  @Override
  public String toString() {
    return "TestFather{" +
        "sex='" + sex + '\'' +
        '}';
  }
}

运行结果:

Test{name='huihui'sex='null'}

发现虽然对sex进行了复制,但是反序列化结果仍然为null。

现在让TestFather类实现Serializable接口,运行结果如下。所以当我们想要序列化父类的变量时,也需要让父类实现Serializable接口。

Test{name='huihui'sex='12'}

同理,如果Test类中有任何变量是对象,那么该对象的类也需要实现Serializable接口。查看String源代码,确实实现了Serializable接口。大家可以测试一下字段的类不实现Serializable接口的情况,运行会直接报java.io.NotSerializableException异常。

敏感字段加密

如果对于某些字段我们并不想直接暴露出去,需要对其进行加密处理,那么就需要我们自定义序列化和反序列化方法。使用Serializable接口进行序列化时,如果不自定义方法,则默认调用ObjectOutputStream的defaultWriteObject方法和ObjectInputStream的defaultReadObject方法。下面我们来尝试一下自己实现序列化与反序列化过程。

class Test implements Serializable {
  private static final long serialVersionUID = 4335715933640891747L;
  private String name;
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  @Override
  public String toString() {
    return "Test{" +
        "name='" + name + '\'' +
        '}';
  }
  private void writeObject(ObjectOutputStream out) {
    try {
      ObjectOutputStream.PutField putField = out.putFields();
      System.out.println("原name:" + name);
      // 模拟加密
      name = "change";
      putField.put("name", name);
      System.out.println("加密后的name:" + name);
      out.writeFields();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
  private void readObject(ObjectInputStream in) {
    try {
      ObjectInputStream.GetField getField = in.readFields();
      Object object = getField.get("name", "");
      System.out.println("要解密的name:" + object.toString());
      name = "huihui";
    } catch (IOException e) {
      e.printStackTrace();
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    }
  }
}

运行结果:

原name:huihui
加密后的name:change
要解密的name:change
解密后的name:huihui

这种写法重写了writeObject方法和readObject方法,下面一种接口也可以实现相同的功能。

Externalizable接口

除了Serializable接口,Java还提供了一个Externalizable接口,它继承了Serializable接口,提供了writeExternal和readExternal两个方法,实现该接口的类必须重写这两个方法。同时还发现,类还必须提供一个无参构造方法,否则会报java.io.InvalidClassException异常。

先不深究为什么要加一个无参构造方法,我们先试一下这个接口的序列化效果。将类Test改为如下所示:

class Test implements Externalizable {
  private static final long serialVersionUID = 4335715933640891747L;
  private String name;
  public Test() {
  }
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  @Override
  public String toString() {
    return "Test{" +
        "name='" + name + '\'' +
        '}';
  }
  @Override
  public void writeExternal(ObjectOutput out) throws IOException {
  }
  @Override
  public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
  }
}

再次运行测试方法,发现输出的name是null。在readObject处打断点,发现会调用无参构造方法。

name其实并没有被序列化与反序列化,writeExternal方法和readExternal方法中是需要我们自己来实现序列化与反序列化的细节的。在反序列化时,会首先调用类的无参考构造方法创建一个新对象,然后再填充每个字段。

我们对writeExternal方法和readExternal方法进行重写:

@Override
public void writeExternal(ObjectOutput out) throws IOException {
  out.writeObject(name);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
  name = (String) in.readObject();
}

此时运行测试方法,发现Test类被正常序列化与反序列化。

序列化存储规则

当多次序列化一个对象时,是会序列化多次还是会序列化一次呢?

public class SerialTest {
  public static void main(String[] args) {
    Test test = new Test();
    test.setName("huihui");
    // 序列化,存储对象到文本
    ObjectOutputStream oos = null;
    try {
      oos = new ObjectOutputStream(new FileOutputStream("test"));
      // 两次写入文件
      oos.writeObject(test);
      oos.flush();
      System.out.println(new File("test").length());
      oos.writeObject(test);
      oos.flush();
      System.out.println(new File("test").length());
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      try {
        if (oos != null) {
          oos.close();
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    // 反序列化,从文本中取出对象
    ObjectInputStream ois = null;
    try {
      ois = new ObjectInputStream(new FileInputStream("test"));
      // 读取两个对象
      Test test1 = (Test) ois.readObject();
      Test test2 = (Test) ois.readObject();
      System.out.println(test1 == test1);
    } catch (IOException | ClassNotFoundException e) {
      e.printStackTrace();
    } finally {
      try {
        if (ois != null) {
          ois.close();
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}
class Test implements Serializable {
  private static final long serialVersionUID = 4335715933640891747L;
  private String name;
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  @Override
  public String toString() {
    return "Test{" +
        "name='" + name + '\'' +
        '}';
  }
}

运行结果:

73
78
true

可以发现,当第二次写入对象时,文件的长度仅仅增加了5个字节,并且在判等时,两个引用指向同一地址。

Java序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。如果你想了解更多相关内容请查看下面相关链接

(0)

相关推荐

  • Java对象的序列化与反序列化详解

    一.序列化和反序列化的概念 把对象转换为字节序列的过程称为对象的序列化,把字节序列恢复为对象的过程称为对象的反序列化. 对象的序列化主要有两种途径: Ⅰ . 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中 Ⅱ.  在网络上传送对象的字节序列. 当两个进程在进行远程通信时,彼此可以发送各种类型的数据.无论是何种类型的数据,都会以二进制序列的形式在网络上传送.发送方需要把这个Java对象转换为字节序列,才能在网络上传送:接收方则需要把字节序列再恢复为Java对象. 二.序列化API 1.

  • Java将对象写入文件读出_序列化与反序列化的实例

    Java类中对象的序列化工作是通过ObjectOutputStream和ObjectInputStream来完成的. 写入: File aFile=new File("e:\\c.txt"); Stu a=new Stu(1, "aa", "1"); FileOutputStream fileOutputStream=null; try { fileOutputStream = new FileOutputStream(aFile); Objec

  • java 序列化与反序列化的实例详解

     1.Java序列化与反序列化 Java序列化是指把Java对象转换为字节序列的过程:而Java反序列化是指把字节序列恢复为Java对象的过程.  2.为什么需要序列化与反序列化 我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本.图片.音频.视频等, 而这些数据都会以二进制序列的形式在网络上传送.那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的.如何做到呢?这就需要Java序列化与反序列化了.换句话说,一方面,发送方需要把这个Java对象转换为字

  • Java 序列化和反序列化实例详解

    Java 序列化和反序列化实例详解 在分布式应用中,对象只有经过序列化才能在各个分布式组件之间传输,这就涉及到两个方面的技术-发送者将对象序列化,接受者将对象反序列化,下面就是一个很好的例子! 1.实体-Employee import java.io.Serializable; public class Employee implements Serializable{ /** * */ private static final long serialVersionUID = 1L; publi

  • Java对象的XML序列化与反序列化实例解析

    上一篇文章我们介绍了java实现的各种排序算法代码示例,本文我们看看Java对象的xml序列化与反序列化的相关内容,具体如下. XML是一种标准的数据交换规范,可以方便地用于在应用之间交换各类数据.如果能在Java对象和XML文档之间建立某种映射,例如Java对象的XML序列化和反序列化,那么就可以使Java的对象方便地与其他应用进行交换. java.beans包里面有两个类XMLEncoder和Decoder,分别用于将符合JabaBeans规范的Java对象以XML方式序列化和反序列化.以下

  • java中fastjson生成和解析json数据(序列化和反序列化数据)

    本文讲解2点: 1. fastjson生成和解析json数据 (举例:4种常用类型:JavaBean,List<JavaBean>,List<String>,List<Map<String,Object>) 2.通过一个android程序测试fastjson的用法. fastjson简介: Fastjson是一个Java语言编写的高性能功能完善的JSON库.fastjson采用独创的算法,将parse的速度提升到极致,超过所有json库,包括曾经号称最快的jack

  • Java实现序列化与反序列化的简单示例

    1.Java序列化与反序列化 Java序列化是指把Java对象转换为字节序列的过程:而Java反序列化是指把字节序列恢复为Java对象的过程. 2.为什么需要序列化与反序列化 我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本.图片.音频.视频等, 而这些数据都会以二进制序列的形式在网络上传送.那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的.如何做到呢?这就需要Java序列化与反序列化了.换句话说,一方面,发送方需要把这个Java对象转换为字节序

  • java 对象的序列化和反序列化详细介绍

    最近周末,对java 的基础知识做了一个整理,其中java 序列化和反序列化的资料进行了详细整理,这里做个笔记,希望也能帮助到读到此文的朋友. 一.序列化和反序列化的概念 把对象转换为字节序列的过程称为对象的序列化. 把字节序列恢复为对象的过程称为对象的反序列化. 对象的序列化主要有两种用途: 1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中: 2) 在网络上传送对象的字节序列. 在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存.比如最常见的是

  • GSON实现Java对象的JSON序列化与反序列化的实例教程

    从GitHub下载GSON:https://github.com/google/gson Gson的应用主要为toJson与fromJson两个转换函数,而在使用这种对象转换之前需先创建好对象的类别以及其成员才能成功的将JSON字符串成功转换成相对应的对象. class Examples { private int answer1 = 100; private String answer2 = "Hello world!"; Examples(){ } // default const

  • 理解Java的序列化与反序列化

    文章主要涉及到以下几个问题: 怎么实现Java的序列化 为什么实现了java.io.Serializable接口才能被序列化 transient的作用是什么 怎么自定义序列化策略 自定义的序列化策略是如何被调用的 ArrayList对序列化的实现有什么好处 一.Java对象的序列化 Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长.但在现实应用中,就可能要求在JVM停止运行之后能够保存

随机推荐