Java中的序列化

Posted codeloong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中的序列化相关的知识,希望对你有一定的参考价值。

序列化是什么意思,能不能给我通俗的讲一下?

序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。 为什么要把Java对象序列化呢?因为序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。
有序列化,就有反序列化,即把一个二进制内容(也就是byte[]数组)变回Java对象。有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象。

为什么需要要序列化?

  • 一般Java对象的生命周期比Java虚拟机短,而实际的开发中,我们需要在Jvm停止后能够继续持有对象,这个时候就需要用到序列化技术将对象持久到磁盘或数据库。
  • 在多个项目进行RPC调用的,需要在网络上传输JavaBean对象。我们知道数据只能以二进制的形式才能在网络上进行传输。所以也需要用到序列化技术。

Java序列化的实现

在Java中,要把一个对象序列化,必须要实现下面两个接口之一:

  • Serializble
  • Externalizable

Serializble接口

一个对象想要被序列化,那么它的类就要实现此接口或者它的子接口。
这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传输。不想序列化的字段可以使用transient修饰。

Externalizable接口

它是Serializable接口的子类,用户要实现的writeExternal()和readExternal() 方法,用来决定如何序列化和反序列化。

因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,而transient在这里无效。

对Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常,因此Externalizable对象必须有默认构造函数,而且必需是public的。

两者之间的对比

使用时,你只想隐藏一个属性,比如用户对象user的密码pwd,如果使用Externalizable,并除了pwd之外的每个属性都写在writeExternal()方法里,这样显得麻烦,可以使用Serializable接口,并在要隐藏的属性pwd前面加transient就可以实现了。如果要定义很多的特殊处理,就可以使用Externalizable。

当然这里我们有一些疑惑,Serializable 中的writeObject()方法与readObject()方法科可以实现自定义序列化,而Externalizable 中的writeExternal()和readExternal() 方法也可以,他们有什么异同呢?

  • readExternal(),writeExternal()两个方法,这两个方法除了方法签名和readObject(),writeObject()两个方法的方法签名不同之外,其方法体完全一样。

  • 需要指出的是,当使用Externalizable机制反序列化该对象时,程序会使用public的无参构造器创建实例,然后才执行readExternal()方法进行反序列化,因此实现Externalizable的序列化类必须提供public的无参构造。

  • 虽然实现Externalizable接口能带来一定的性能提升,但由于实现ExternaLizable接口导致了编程复杂度的增加,所以大部分时候都是采用实现Serializable接口方式来实现序列化。

序列化版本号

在Java序列化中,可以控制序列化的版本,该字段为被序列化对象中的serialVersionUID字段。

private static final long serialVersionUID = 1L;

一个对象数据,在反序列化过程中,如果序列化串中的serialVersionUID与当前对象值不同,则反序列化失败,否则成功。

如果serialVersionUID没有显式生成,系统就会自动生成一个。生成的输入有:类名、类及其属性修饰符、接口及接口顺序、属性、静态初始化、构造器。任何一项的改变都会导致serialVersionUID变化。

为了避免这种问题, 一般系统都会要求实现serialiable接口的类显式的生明一个serialVersionUID。

显式定义serialVersionUID的两种用途:

  • 希望类的不同版本对序列化兼容时,需要确保类的不同版本具有相同的serialVersionUID;

  • 不希望类的不同版本对序列化兼容时,需要确保类的不同版本具有不同的serialVersionUID。如果我们保持了serialVersionUID的一致,则在反序列化时,对于新增的字段会填入默认值null(int的默认值0),对于减少的字段则直接忽略。

序列化代码实现

public class User implements Serializable {

  private static final long serialVersionUID = 1L;

  private String username;
  private String gender;
  private int age;
  private Date birth;
  private Pet pet;

  public User() {
  }

  public User(String username, String gender, int age, Date birth, Pet pet) {
    this.username = username;
    this.gender = gender;
    this.age = age;
    this.birth = birth;
    this.pet = pet;
  }

  //序列化时默认调用
  private void writeObject(ObjectOutputStream out) throws IOException {
    Base64.Encoder encoder = Base64.getEncoder();
    byte[] bytes = encoder.encode(this.username.getBytes());
    this.username = new String(bytes);
    out.defaultWriteObject();
  }

  //反序列化时默认调用
  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    Base64.Decoder decoder = Base64.getDecoder();
    byte[] bytes = decoder.decode(this.username);
    this.username = new String(bytes);
  }
  
  //=======================测试一下==========================
  public static void main(String[] args) {
    User user = new User("jay", "male",
            18, new Date(), new Pet("wangcai", "male"));

    File file = new File("user.dat");
    if (!file.exists()){
      try {
        if(!file.createNewFile()){
          System.out.println("文件创建失败!");
          return;
        }

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file, false));
        oos.writeObject(user);
        oos.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }

  }

  @Test
  public void test(){
    File file = new File("user.dat");
    if (!file.exists()) return;
    try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
      User user = (User) ois.readObject();
      System.out.println(user);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

在网络中传输使用socket模拟即可,不写了

序列化和单例模式

所谓单例:就是单例模式就是在整个全局中(无论是单线程还是多线程),该对象只存在一个实例,而且只应该存在一个实例,没有副本。
序列化对单例有破坏:

  • 1、通过对某个对象的序列化与反序列化得到的对象是一个新的对象,这就破坏了单例模式的单例性。
  • 2、我们知道readObject()的时候,底层运用了反射的技术,序列化会通过反射调用无参数的构造方法创建一个新的对象。这破坏了对象的单例性。
  • 3、解决方案:在需要的单例的对象类中添加如下代码
private Object readResolve(){
	return instance;
}

为什么说序列化并不安全

因为序列化的对象数据转换为二进制,并且完全可逆。但是在RMI调用时所有private字段的数据都以明文二进制的形式出现在网络的套接字上,这显然是不安全的。

解决方案:

  1. 序列化Hook化(移位和复位)
  2. 序列数据加密和签名
  3. 利用transient的特性解决
  4. 打包和解包代理

以上是关于Java中的序列化的主要内容,如果未能解决你的问题,请参考以下文章

(转) Java中的负数及基本类型的转型详解

如何在 Java 中播放(MIDI)序列中的音频剪辑?

有没有办法关闭代码片段中的命名建议?

片段 A 的列表视图中的片段 B 中的新列表视图,单击 A 的列表项

ASP.net MVC 代码片段问题中的 Jqgrid 实现

如何从 Firebase 获取数据到 Recyclerview 中的片段?