关于java:面试官说说你对序列化的理解

7次阅读

共计 12681 个字符,预计需要花费 32 分钟才能阅读完成。

关注“Java 后端技术全栈”

回复“000”获取大量电子书

本文次要内容

背景

在 Java 语言中,程序运行的时候,会产生很多对象,而对象信息也只是在程序运行的时候才在内存中放弃其状态,一旦程序进行,内存开释,对象也就不存在了。

怎么能让对象永恒的保留下来呢?——–对象序列化

何为序列化和反序列化?

  • 序列化:对象到 IO 数据流

  • 反序列化:IO 数据流到对象

有哪些应用场景?

Java 平台容许咱们在内存中创立可复用的 Java 对象,但个别状况下,只有当 JVM 处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比 JVM 的生命周期更长。但在事实利用中,就可能要求在 JVM 进行运行之后可能保留 (长久化) 指定的对象,并在未来从新读取被保留的对象。Java 对象序列化就可能帮忙咱们实现该性能。

应用 Java 对象序列化,在保留对象时,会把其状态保留为一组字节,在将来,再将这些字节组装成对象。必须留神地是,对象序列化保留的是对象的 ” 状态 ”,即它的成员变量。由此可知,对象序列化不会关注类中的动态变量。

除了在长久化对象时会用到对象序列化之外,当应用 RMI(近程办法调用),或在网络中传递对象时,都会用到对象序列化。

Java 序列化 API 为解决对象序列化提供了一个规范机制,该 API 简略易用。

很多框架中都有用到,比方典型的 dubbo 框架中应用了序列化。

序列化有什么作用?

序列化机制容许将实现序列化的 Java 对象转换位字节序列,这些字节序列能够保留在磁盘上,或通过网络传输,以达到当前复原成原来的对象。序列化机制使得对象能够脱离程序的运行而独立存在。

序列化实现形式

Java 语言中,常见实现序列化的形式有两种:

  • 实现 Serializable 接口
  • 实现 Externalizable 接口

上面咱们就来具体的说说这两种实现形式。

实现 Serializable 接口

创立一个 User 类实现 Serializable 接口,实现序列化,大抵步骤为:

  1. 对象实体类实现 Serializable 标记接口。
  2. 创立序列化输入流对象 ObjectOutputStream,该对象的创立依赖于其它输入流对象,通常咱们将对象序列化为文件存储,所以这里用文件相干的输入流对象 FileOutputStream。
  3. 通过 ObjectOutputStream 的 writeObject()办法将对象序列化为文件。
  4. 敞开流。

以下就是 code:

 `package com.tian.my_code.test.clone;`
 
 `import java.io.FileOutputStream;`
 `import java.io.IOException;`
 `import java.io.ObjectOutputStream;`
 `import java.io.Serializable;`
 
 `public class User implements Serializable {`
 `private int age;`
 `private String name;`
 
 `public User() {`
 `}`
 
 `public User(int age, String name) {`
 `this.age = age;`
 `this.name = name;`
 `}`
 `//set get 省略 `
 `public static void main(String[] args) {`
 `try {`
 `ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("user.txt"));`
 `User user=new User(22,"老田");`
 `objectOutputStream.writeObject(user);`
 `} catch (IOException e) {`
 `e.printStackTrace();`
 `}`
 `}`
 `}`

创立一个 User 对象,而后把 User 对象保留的 user.txt 中了。

反序列化

大抵有以下三个步骤:

  1. 创立输出流对象 ObjectOutputStream。同样依赖于其它输出流对象,这里是文件输出流 FileInputStream。
  2. 通过 ObjectInputStream 的 readObject()办法,将文件中的对象读取到内存。
  3. 敞开流。

上面咱们再进行反序列化 code:

 `package com.tian.my_code.test.clone;`
 
 `import java.io.*;`
 
 `public class SeriTest {`
 `public static void main(String[] args) {`
 `try {`
 `ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.txt"));`
 `User user=(User) ois.readObject();`
 `System.out.println(user.getName());`
 `} catch (Exception e) {`
 `e.printStackTrace();`
 `}`
 `}`
 `}`

运行这段代码,输入后果:

应用 IDEA 关上 user.tst 文件:

应用编辑器 16 机制查看

对于文件内容咱们就不必太关怀了,持续说咱们的重点。

序列化是把 User 对象寄存到文件里了,而后反序列化就是读取文件内容并创建对象。

A 端把对象 User 保留到文件 user.txt 中,B 端就能够通过网络或者其余形式读取到这个文件,再进行反序列化,取得 A 端创立的 User 对象。

拓展

如果 B 端拿到的 User 属性如果有变动呢?比如说:减少一个字段

 `private String address;`

再次进行反序列化就会报错

增加 serialVersionUID

 `package com.tian.my_code.test.clone;`
 
 `import java.io.FileOutputStream;`
 `import java.io.IOException;`
 `import java.io.ObjectOutputStream;`
 `import java.io.Serializable;`
 
 `public class User implements Serializable{`
 `private static final long serialVersionUID = 2012965743695714769L;`
 `private int age;`
 `private String name;`
 
 `public User() {`
 `}`
 
 `public User(int age, String name) {`
 `this.age = age;`
 `this.name = name;`
 `}`
 
 `// set get   省略 `
 
 `public static void main(String[] args) {`
 `try {`
 `ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("user.txt"));`
 `User user=new User(22,"老田");`
 `objectOutputStream.writeObject(user);`
 `} catch (IOException e) {`
 `e.printStackTrace();`
 `}`
 `}`
 `}`

再次执行反序列化,运行后果失常

而后咱们再次加上字段和对应的 get/set 办法

 `private String address;`

再次执行反序列化

反序列化胜利。

如果可序列化类未显式申明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值,如“Java(TM) 对象序列化标准”中所述。

不过,强烈建议 所有可序列化类都显式申明 serialVersionUID 值,起因是计算默认的 serialVersionUID 对类的详细信息具备较高的敏感性,依据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。

因而,为保障 serialVersionUID 值跨不同 Java 编译器实现的一致性,序列化类必须申明一个明确的 serialVersionUID 值。

强烈建议应用 private 修饰符显示申明 serialVersionUID(如果可能),起因是这种申明仅利用于间接申明类 — serialVersionUID 字段作为继承成员没有用途。数组类不能申明一个明确的 serialVersionUID,因而它们总是具备默认的计算值,然而数组类没有匹配 serialVersionUID 值的要求。

所以,尽量显示的申明,这样序列化的类即便有字段的批改,因为 serialVersionUID 的存在,也能保障反序列化胜利。保障了更好的兼容性。

IDEA 中如何快捷增加 serialVersionUID?

咱们的类实现 Serializable 接口,鼠标放在类上,Alt+Enter 键就能够增加了。

实现 Externalizable 接口

通过实现 Externalizable 接口,必须实现 writeExternal、readExternal 办法。

`@Override`
`public void writeExternal(ObjectOutput out) throws IOException {`
`}`
`@Override`
`public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {`
 
`}`

Externalizable 是 Serializable 的子接口。

`public interface Externalizable extends java.io.Serializable {`

持续应用后面的 User,代码进行革新:

 `package com.tian.my_code.test.clone;`
 
 `import java.io.*;`
 
 `public class User implements Externalizable {`
 `private int age;`
 `private String name;`
 
 `public User() {`
 `}`
 
 `public User(int age, String name) {`
 `this.age = age;`
 `this.name = name;`
 `}`
 
 `//set get`
 
 
 `public static void main(String[] args) {`
 `try {`
 `ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("user.txt"));`
 `User user = new User(22, "老田");`
 `objectOutputStream.writeObject(user);`
 `} catch (IOException e) {`
 `e.printStackTrace();`
 `}`
 `}`
 
 `@Override`
 `public void writeExternal(ObjectOutput out) throws IOException {`
 `// 将 name 反转后写入二进制流 `
 `StringBuffer reverse = new StringBuffer(name).reverse();`
 `out.writeObject(reverse);`
 `out.writeInt(age);`
 `}`
 
 `@Override`
 `public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {`
 `// 将读取的字符串反转后赋值给 name 实例变量 `
 `this.name = ((StringBuffer) in.readObject()).reverse().toString();`
 `// 将读取到的 int 类型值付给 age`
 `this.age = in.readInt();`
 `}`
 `}`
 

执行序列化,而后再次执行反序列化,输入:

留神

Externalizable 接口不同于 Serializable 接口,实现此接口必须实现接口中的两个办法实现自定义序列化,这是强制性的;特别之处是必须提供 public 的无参结构器,因为在反序列化的时候须要反射创建对象。

两种形式比照

下图为两种实现形式的比照:

序列化只有两种形式吗?

当然不是。依据序列化的定义,不论通过什么形式,只有你能把内存中的对象转换成能存储或传输的形式,又能反过来复原它,其实都能够称为序列化。因而,咱们罕用的 Fastjson、Jackson 等第三方类库将对象转成 Json 格式文件,也能够算是一种序列化,用JAXB 实现 XML 格式文件输入,也能够算是序列化。所以,千万不要被思维局限,其实事实当中咱们进行了很多序列化和反序列化的操作,波及不同的状态、数据格式等。

序列化算法

  • 所有保留到磁盘的对象都有一个序列化编码号。
  • 当程序试图序列化一个对象时,会先查看此对象是否曾经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输入。
  • 如果此对象曾经序列化过,则间接输入编号即可。

自定义序列化

有些时候,咱们有这样的需要,某些属性不须要序列化。应用 transient 关键字抉择不须要序列化的字段。

持续应用后面的代码进行革新,在 age 字段上增加 transient 润饰:

 `package com.tian.my_code.test.clone;`
 
 `import java.io.FileOutputStream;`
 `import java.io.IOException;`
 `import java.io.ObjectOutputStream;`
 `import java.io.Serializable;`
 
 `public class User implements Serializable{`
 `private transient int age;`
 `private String name;`
 
 
 `public User() {`
 `}`
 
 `public User(int age, String name) {`
 `this.age = age;`
 `this.name = name;`
 `}`
 
 `public int getAge() {`
 `return age;`
 `}`
 
 `public void setAge(int age) {`
 `this.age = age;`
 `}`
 
 `public String getName() {`
 `return name;`
 `}`
 
 `public void setName(String name) {`
 `this.name = name;`
 `}`
 
 `public static void main(String[] args) {`
 `try {`
 `ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("user.txt"));`
 `User user=new User(22,"老田");`
 `objectOutputStream.writeObject(user);`
 `} catch (IOException e) {`
 `e.printStackTrace();`
 `}`
 `}`
 `}`
 ` ``` `
` 序列化,而后进行反序列化:`
` ```java`
 `package com.tian.my_code.test.clone;`
 
 `import java.io.*;`
 
 `public class SeriTest {`
 `public static void main(String[] args) {`
 `try {`
 `ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.txt"));`
 `User user=(User) ois.readObject();`
 `System.out.println(user.getName());`
 `System.out.println(user.getAge());`
 `} catch (Exception e) {`
 `e.printStackTrace();`
 `}`
 `}`
 `}`

运行输入:

从输入咱们看到,应用 transient 润饰的属性,Java 序列化时,会疏忽掉此字段,所以反序列化出的对象,被 transient 润饰的属性是默认值。

对于援用类型,值是 null;根本类型,值是 0;boolean 类型,值是 false。

摸索

到此序列化内容算讲完了,然而,如果只停留在这个层面,是无奈应答理论工作中的问题的。

比方模型对象持有其它对象的援用怎么解决,援用类型如果是简单些的汇合类型怎么解决?

下面的 User 中持有 String 援用类型的,照样序列化没问题,那么如果是咱们自定义的援用类呢?

比方上面的场景:

 `package com.tian.my_code.test.clone;`
 
 `public class UserAddress {`
 `private int provinceCode;`
 `private int cityCode;`
 
 `public UserAddress() {`
 `}`
 
 `public UserAddress(int provinceCode, int cityCode) {`
 `this.provinceCode = provinceCode;`
 `this.cityCode = cityCode;`
 `}`
 
 `public int getProvinceCode() {`
 `return provinceCode;`
 `}`
 
 `public void setProvinceCode(int provinceCode) {`
 `this.provinceCode = provinceCode;`
 `}`
 
 `public int getCityCode() {`
 `return cityCode;`
 `}`
 
 `public void setCityCode(int cityCode) {`
 `this.cityCode = cityCode;`
 `}`
 `}`

而后在 User 中增加一个 UserAddress 的属性:

 `package com.tian.my_code.test.clone;`
 
 `import java.io.FileOutputStream;`
 `import java.io.IOException;`
 `import java.io.ObjectOutputStream;`
 `import java.io.Serializable;`
 
 `public class User implements Serializable{`
 `private static final long serialVersionUID = -2445226500651941044L;`
 `private int age;`
 `private String name;`
 `private UserAddress userAddress;`
 
 `public User() {`
 `}`
 
 `public User(int age, String name) {`
 `this.age = age;`
 `this.name = name;`
 `}`
 `//get set`
 
 `public static void main(String[] args) {`
 `try {`
 `ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("user.txt"));`
 `User user=new User(22,"老田");`
 `UserAddress userAddress=new UserAddress(10001,10001001);`
 `user.setUserAddress(userAddress);`
 `objectOutputStream.writeObject(user);`
 `} catch (IOException e) {`
 `e.printStackTrace();`
 `}`
 `}`
 `}`

运行下面代码:

抛出了 java.io.NotSerializableException 异样。很显著在通知咱们,UserAddress 没有实现序列化接口。待 UserAddress 类实现序列化接口后:

 `package com.tian.my_code.test.clone;`
 
 `import java.io.Serializable;`
 
 `public class UserAddress implements Serializable {`
 `private static final long serialVersionUID = 5128703296815173156L;`
 `private int provinceCode;`
 `private int cityCode;`
 
 `public UserAddress() {`
 `}`
 
 `public UserAddress(int provinceCode, int cityCode) {`
 `this.provinceCode = provinceCode;`
 `this.cityCode = cityCode;`
 `}`
 `//get set`
 `}`

再次运行,失常不报错了。

反序列化代码:

 `package com.tian.my_code.test.clone;`
 
 `import java.io.*;`
 
 `public class SeriTest {`
 `public static void main(String[] args) {`
 `try {`
 `ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.txt"));`
 `User user=(User) ois.readObject();`
 `System.out.println(user.getName());`
 `System.out.println(user.getAge());`
 `System.out.println(user.getUserAddress().getProvinceCode());`
 `System.out.println(user.getUserAddress().getCityCode());`
 `} catch (Exception e) {`
 `e.printStackTrace();`
 `}`
 `}`
 `}`

运行后果:

典型使用场景

 `public final class String implements java.io.Serializable, Comparable<String>, CharSequence {`
 `private static final long serialVersionUID = -6849794470754667710L;`
 `}`
 `public class HashMap<K,V> extends AbstractMap<K,V>  implements Map<K,V>, Cloneable, Serializable {`
 `private static final long serialVersionUID = 362498820763181265L;`
 `}`
 `public class ArrayList<E> extends AbstractList<E>  implements List<E>, RandomAccess, Cloneable, java.io.Serializable{`
 `private static final long serialVersionUID = 8683452581122892189L;`
 `}`
 `.....`

很多罕用类都实现了序列化接口。

再次拓展

下面说的 transient 反序列化的时候是默认值,然而你会发现,几种罕用汇合类 ArrayList、HashMap、LinkedList 等数据存储字段,居然都被 transient  润饰了,然而在实际操作中咱们用汇合类型存储的数据却能够被失常的序列化和反序列化?

假相当然还是在源码里。实际上,各个汇合类型对于序列化和反序列化是有独自的实现的,并没有采纳虚拟机默认的形式。这里以 ArrayList 中的序列化和反序列化源码局部为例剖析:

 `private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{`
 `int expectedModCount = modCount;`
 `// 序列化以后 ArrayList 中非 transient 以及非动态字段 `
 `s.defaultWriteObject();`
 `// 序列化数组理论个数 `
 `s.writeInt(size);`
 `// 一一取出数组中的值进行序列化 `
 `for (int i=0; i<size; i++) {`
 `s.writeObject(elementData[i]);`
 `}`
 `// 避免在并发的状况下对元素的批改 `
 `if (modCount != expectedModCount) {`
 `throw new ConcurrentModificationException();`
 `}`
 `}`
 
 `private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {`
 `elementData = EMPTY_ELEMENTDATA;`
 `// 反序列化非 transient 以及非动态润饰的字段,其中蕴含序列化时的数组大小 size`
 `s.defaultReadObject();`
 `// 疏忽的操作 `
 `s.readInt(); // ignored`
 `if (size > 0) {`
 `// 容量计算 `
 `int capacity = calculateCapacity(elementData, size);`
 `SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);`
 `// 检测是否须要对数组扩容操作 `
 `ensureCapacityInternal(size);`
 `Object[] a = elementData;`
 `// 按程序反序列化数组中的值 `
 `for (int i=0; i<size; i++) {`
 `a[i] = s.readObject();`
 `}`
 `}`
 `}`

读源码能够晓得,ArrayList 的序列化和反序列化次要思路就是依据汇合中理论存储的元素个数来进行操作,这样做预计是为了防止不必要的空间节约(因为 ArrayList 的扩容机制决定了,汇合中理论存储的元素个数必定比汇合的可容量要小)。为了验证,咱们能够在单元测试序列化和返序列化的时候,在 ArrayLIst 的两个办法中打上断点,以确认这两个办法在序列化和返序列化的执行流程中(截图为反序列化过程):

原来,咱们之前自认为汇合能胜利序列化也只是简略的实现了标记接口都只是表象,表象背地有各个汇合类有不同的深意。所以,同样的思路,读者敌人能够本人去剖析下 HashMap 以及其它汇合类中自行管制序列化和反序列化的个中门道了,感兴趣的小伙伴能够自行去查看一番。

序列化注意事项

1、序列化时,只对对象的状态进行保留,而不论对象的办法;

2、当一个父类实现序列化,子类主动实现序列化,不须要显式实现 Serializable 接口;

3、当一个对象的实例变量援用其余对象,序列化该对象时也把援用对象进行序列化;

4、并非所有的对象都能够序列化,至于为什么不能够,有很多起因了,比方:

  • 平安方面的起因,比方一个对象领有 private,public 等 field,对于一个要传输的对象,比方写到文件,或者进行 RMI 传输等等,在序列化进行传输的过程中,这个对象的 private 等域是不受爱护的;
  • 资源分配方面的起因,比方 socket,thread 类,如果能够序列化,进行传输或者保留,也无奈对他们进行从新的资源分配,而且,也是没有必要这样实现;

5、申明为 static 和 transient 类型的成员数据不能被序列化。因为 static 代表类的状态,transient 代表对象的长期数据。

6、序列化运行时应用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义 serialVersionUID 有两种用处:

  • 在某些场合,心愿类的不同版本对序列化兼容,因而须要确保类的不同版本具备雷同的 serialVersionUID;
  • 在某些场合,不心愿类的不同版本对序列化兼容,因而须要确保类的不同版本具备不同的 serialVersionUID。

7、Java 有很多根底类曾经实现了 serializable 接口,比方 String,Vector 等。然而也有一些没有实现 serializable 接口的;

8、如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保留!这是能用序列化解决深拷贝的重要起因;

总结

什么是序列化?序列化 Java 中罕用实现形式有哪些?两种实现序列化形式的比照,序列化算法?如何自定义序列化?Java 汇合框架中序列化是如何实现的?

这几个点如果没有 get 到,麻烦请再次浏览,或者加我微信进群里大家一起聊。

参考:

cnblogs.com/chenbenbuyi/p/10741195.html cnblogs.com/9dragon/p/10901448.html oschina.net/translate/serialization-in-java

业余分享 java 相干技术,欢送关注~

举荐浏览

吊打面试官系列:说说反射的用处及实现?

6000 多字 | 秒杀零碎设计留神点

正文完
 0