Java序列化面试总结

Java序列化与反序列化是什么?

Java序列化是指把Java对象转换为字节流的过程,而Java反序列化是指把字节流恢复为Java对象的过程。

序列化: 序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。核心作用是对象状态的保存与重建。我们都知道,Java对象是保存在JVM的堆内存中的,也就是说,如果JVM堆不存在了,那么对象也就跟着消失了。

而序列化提供了一种方案,可以让你在即使JVM停机的情况下也能把对象保存下来的方案。就像我们平时用的U盘一样。把Java对象序列化成可存储或传输的形式(如二进制流),比如保存在文件中。这样,当再次需要这个对象的时候,从文件中读取出二进制流,再从二进制流中反序列化出对象。

反序列化: 客户端从文件中或网络上获得序列化后的对象字节流,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。

序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。

序列化和反序列化常见应用场景

  • 对象在进行网络传输(比如远程过程调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;

  • 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;

  • 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;

  • 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。

需要注意的是,虽然将对象序列化到内存中可以减少与磁盘或者网络的交互,但是仍然需要占用内存空间。如果需要处理大量的对象,可能会导致内存问题,因此应该根据实际情况来选择是否将对象序列化到内存中。

为什么需要序列化与反序列化?

简要描述:对内存中的对象进行持久化或网络传输, 这个时候都需要序列化和反序列化。

深入描述:

1.对象序列化可以实现分布式对象。

主要应用例如:RMI(即远程调用Remote Method Invocation)要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样。

2.java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。

可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的"深复制",即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。

3.序列化可以将内存中的类写入文件或数据库中。

比如:将某个类序列化后存为文件,下次读取时只需将文件中的数据反序列化就可以将原先的类还原到内存中。也可以将类序列化为流数据进行传输。
总的来说就是将一个已经实例化的类转成文件存储,下次需要实例化的时候只要反序列化即可将类实例化到内存中并保留序列化时类中的所有变量和状态。

4.对象、文件、数据,有许多不同的格式,很难统一传输和保存。

序列化以后就都是字节流了,无论原来是什么东西,都能变成一样的东西,就可以进行通用的格式传输或保存,传输结束以后,要再次使用,就进行反序列化还原,这样对象还是对象,文件还是文件。

常见序列化协议有哪些?

JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。

比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。

像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。

JDK 自带的序列化方式

实现 Serializable 接口或者 Externalizable 接口。

Serializable接口

类通过实现 java.io.Serializable 接口以启用其序列化功能。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。

如以下例子:

import java.io.Serializable;

public class User implements Serializable {
   private static final long serialVersionUID = 1905122041950251207L;
   private String name;
   private int age;
   public String getName() {
       return name;
   }
   public void setName(String name) {
       this.name = name;
   }

   @Override
   public String toString() {
       return "User{" +
               "name='" + name +
               '}';
   }
}

通过下面的代码进行序列化及反序列化:

public class SerializableDemo {

   public static void main(String[] args) {
       //Initializes The Object
       User user = new User();
       user.setName("cosen");
       System.out.println(user);

       //Write Obj to File
       try (FileOutputStream fos = new FileOutputStream("tempFile"); ObjectOutputStream oos = new ObjectOutputStream(
           fos)) {
           oos.writeObject(user);
       } catch (IOException e) {
           e.printStackTrace();
       }

       //Read Obj from File
       File file = new File("tempFile");
       try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
           User newUser = (User)ois.readObject();
           System.out.println(newUser);
       } catch (IOException | ClassNotFoundException e) {
           e.printStackTrace();
       }
   }
}

//OutPut:
//User{name='cosen'}
//User{name='cosen'}

Externalizable接口

Externalizable继承自Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。

当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法。否则所有变量的值都会变成默认值。

public class User implements Externalizable {
   private static final long serialVersionUID = 1905122041950251207L;
   private String name;
   private int age;

   public String getName() {
       return name;
   }
   public void setName(String name) {
       this.name = name;
   }
   public void writeExternal(ObjectOutput out) throws IOException {
       out.writeObject(name);
   }
   public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
       name = (String) in.readObject();
   }

   @Override
   public String toString() {
       return "User{" +
               "name='" + name +
               '}';
   }
}

通过下面的代码进行序列化及反序列化:

public class ExternalizableDemo1 {

  public static void main(String[] args) {
      //Write Obj to file
      User user = new User();
      user.setName("cosen");
      try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"))){
          oos.writeObject(user);
      } catch (IOException e) {
          e.printStackTrace();
      }

      //Read Obj from file
      File file = new File("tempFile");
      try(ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file))){
          User newInstance = (User) ois.readObject();
          //output
          System.out.println(newInstance);
      } catch (IOException | ClassNotFoundException e ) {
          e.printStackTrace();
      }
  }
}

//OutPut:
//User{name='cosen'}

两种序列化的对比

实现Serializable接口 实现Externalizable接口
系统自动存储必要的信息 程序员决定存储哪些信息
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持 必须实现接口内的两个方法
性能略差 性能略好

serialVersionUID 有什么作用?

serialVersionUID 用来表明类的不同版本间的兼容性,起版本控制的作用。

Java的序列化机制是通过在运行时判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的serialVersionUID 与本地相应实体(类)的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidClassException)。

为什么还要显示指定serialVersionUID的值?

如果不显示指定serialVersionUID,JVM在序列化时会根据属性自动生成一个serialVersionUID,然后与属性一起序列化,,再进行持久化或网络传输。在反序列化时, JVM会再根据属性自动生成一个新版serialVersionUID, 然后将这个新版serialVersionUID与序列化时生成的旧版serialVersionUID进行比较, 如果相同则反序列化成功, 否则报错.

如果显示指定了,JVM在序列化和反序列化时仍然都会生成一个serialVersionUID。但值为我们显示指定的值, 这样在反序列化时新旧版本的serialVersionUID就一致了.

在实际开发中, 不显示指定serialVersionUID的情况会导致什么问题? 如果我们的类写完后不再修改, 那当然不会有问题, 但这在实际开发中是不可能的, 我们的类会不断迭代, 一旦类被修改了, 那旧对象反序列化就会报错. 所以在实际开发中, 我们都会显示指定一个serialVersionUID, 值是多少无所谓, 只要不变就行。

serialVersionUID 不是被 static 变量修饰了吗?为什么还会被“序列化”?

static 修饰的变量是静态变量,位于方法区,本身是不会被序列化的。 static 变量是属于类的而不是对象。serialVersionUID 属性并没有被序列化,JVM在序列化对象时会自动生成一个serialVersionUID, 然后将我们显示指定的 serialVersionUID 属性值赋给自动生成的serialVersionUID。

serialVersionUID什么时候修改?

《阿里巴巴Java开发手册》中有以下规定:
在这里插入图片描述

Java 序列化中如果有些字段不想进行序列化,怎么办?

对于不想进行序列化的变量,使用 transient 关键字修饰。

transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。transient 只能修饰变量,不能修饰类和方法。

关于 transient 还有几点注意:

  • transient 只能修饰变量,不能修饰类和方法。

  • transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0。

  • static 变量因为不属于任何对象(Object),所以无论有没有transient 关键字修饰,均不会被序列化。

静态变量会被序列化吗?

不会。因为序列化是针对对象而言的, 而静态变量优先于对象存在, 随着类的加载而加载, 所以不会被序列化.

看到这个结论, 是不是有人会问, serialVersionUID也被static修饰, 为什么serialVersionUID会被序列化? 其实serialVersionUID属性并没有被序列化, JVM在序列化对象时会自动生成一个serialVersionUID, 然后将我们显示指定的serialVersionUID属性值赋给自动生成的serialVersionUID。

为什么不推荐使用 JDK 自带的序列化?

我们很少或者说几乎不会直接使用 JDK 自带的序列化方式,主要原因有下面这些原因:

  • 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。

  • 性能差 :相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。

  • 存在安全问题 :序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。

如果想要详细了解这个问题,可以参考这篇文章——应用安全:JAVA反序列化漏洞之殇。

你可能感兴趣的:(#,Java面试总结,java,面试,jvm)