关注“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多字 | 秒杀零碎设计留神点