Java中如何实现序列化与反序列化

这篇文章给大家介绍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

关于Java中如何实现序列化与反序列化就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。


本文标题:Java中如何实现序列化与反序列化
URL链接:http://scyanting.com/article/geidie.html