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

本文主要内容java

5d1707fc4b49a48d047d8e049ada0ab4.jpg

背景

在Java语言中,程序运行的时候,会产生不少对象,而对象信息也只是在程序运行的时候才在内存中保持其状态,一旦程序中止,内存释放,对象也就不存在了。算法

怎么能让对象永久的保存下来呢?--------对象序列化json

何为序列化和反序列化?数组

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

    e23940375dcf62e32f64bb3d574fd2b7.jpg

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

    da8a5120939780ed2f6391cf77360f65.jpg

有哪些使用场景?

Java平台容许咱们在内存中建立可复用的Java对象,但通常状况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM中止运行以后可以保存(持久化)指定的对象,并在未来从新读取被保存的对象。Java对象序列化就可以帮助咱们实现该功能。安全

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

除了在持久化对象时会用到对象序列化以外,当使用RMI(远程方法调用),或在网络中传递对象时,都会用到对象序列化。网络

Java序列化API为处理对象序列化提供了一个标准机制,该API简单易用。并发

不少框架中都有用到,好比典型的dubbo框架中使用了序列化。框架

序列化有什么做用?

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

序列化实现方式

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();
            }
        }
    }

运行这段代码,输出结果:

1637615e1614b519a5ee8ba65298b993.jpg

使用IDEA打开user.tst文件:

aa7a95953a65929e156e80c205129d15.jpg

使用编辑器16机制查看

3b7ed478e939ea13f995c60975b79be6.jpg

关于文件内容我们就不用太关心了,继续说咱们的重点。

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

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

拓展

若是B端拿到的User属性若是有变化呢?好比说:增长一个字段

    private String address;

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

6d9f4edbe5dcf7b61f8fbcdb0b174dd6.jpg

添加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();
            }
        }
    }

再次执行反序列化,运行结果正常

82b32e287f07c3cd9d3e9d951b48dc6d.jpg

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

    private String address;

再次执行反序列化

69393f06c750ad985cf86201fcfe8f9d.jpg

反序列化成功。

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

不过,强烈建议 全部可序列化类都显式声明 serialVersionUID 值,缘由是计算默认的 serialVersionUID对类的详细信息具备较高的敏感性,根据编译器实现的不一样可能千差万别,这样在反序列化过程当中可能会致使意外的 InvalidClassException。

所以,为保证 serialVersionUID值跨不一样 Java 编译器实现的一致性,序列化类必须声明一个明确的 serialVersionUID值。

强烈建议使用 private 修饰符显示声明 serialVersionUID(若是可能),缘由是这种声明仅应用于直接声明类 -- serialVersionUID字段做为继承成员没有用处。数组类不能声明一个明确的 serialVersionUID,所以它们老是具备默认的计算值,可是数组类没有匹配 serialVersionUID值的要求。

因此,尽可能显示的声明,这样序列化的类即便有字段的修改,由于 serialVersionUID的存在,也能保证反序列化成功。保证了更好的兼容性。

IDEA中如何快捷添加serialVersionUID?

ab948162b6251d0a68aca6fc22135dad.jpg

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

实现Externalizable接口

经过实现Externalizable接口,必须实现writeExternal、readExternal方法。

07356b54a9af8615c4e3268e3fc9db1e.jpg

cf248a3f8aec6d7e7cfe76caf8dbba00.jpg

@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();
        }
    }
    

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

85f6861bee6f1f599148e92c8c607162.jpg

注意

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

两种方式对比

下图为两种实现方式的对比:

0a33d7d44b74b2c70ea05565e58b917c.jpg

序列化只有两种方式吗?

固然不是。根据序列化的定义,无论经过什么方式,只要你能把内存中的对象转换成能存储或传输的方式,又能反过来恢复它,其实均可以称为序列化。所以,咱们经常使用的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();
            }
        }
    }

运行输出:

241063a260371e7b5d47debd92b3679c.jpg

从输出咱们看到,使用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();
            }
        }
    }

运行上面代码:

ff3eb7b7bbdcbf57e00f07f445f4e2d9.jpg

抛出了 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();
            }
        }
    }

运行结果:

af15153b46be0d93b1f93af865da3e8d.jpg

典型运用场景

    public final class String implements java.io.SerializableComparable<String>, CharSequence {
        private static final long serialVersionUID = -6849794470754667710L;
    }
    public class HashMap<K,Vextends AbstractMap<K,V>  implements Map<K,V>, CloneableSerializable {
        private static final long serialVersionUID = 362498820763181265L;
    }
    public class ArrayList<Eextends AbstractList<E>  implements List<E>, RandomAccessCloneablejava.io.Serializable{
        private static final long serialVersionUID = 8683452581122892189L;
    }
    .....

不少经常使用类都实现了序列化接口。

再次拓展

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

cd811e29b606d965cbf08924442fdec8.jpg

真至关然仍是在源码里。实际上,各个集合类型对于序列化和反序列化是有单独的实现的,并无采用虚拟机默认的方式。这里以 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[].classcapacity);
                //检测是否须要对数组扩容操做
                ensureCapacityInternal(size);
                Object[] a = elementData;
                // 按顺序反序列化数组中的值
                for (int i=0; i<size; i++) {
                    a[i] = s.readObject();
                }
            }
        }

读源码能够知道,ArrayList的序列化和反序列化主要思路就是根据集合中实际存储的元素个数来进行操做,这样作估计是为了不没必要要的空间浪费(由于ArrayList的扩容机制决定了,集合中实际存储的元素个数确定比集合的可容量要小)。为了验证,咱们能够在单元测试序列化和返序列化的时候,在ArrayLIst的两个方法中打上断点,以确认这两个方法在序列化和返序列化的执行流程中(截图为反序列化过程):

原来,咱们以前自觉得集合能成功序列化也只是简单的实现了标记接口都只是表象,表象背后有各个集合类有不一样的深意。因此,一样的思路,读者朋友能够本身去分析下 HashMap以及其它集合类中自行控制序列化和反序列化的个中门道了,感兴趣的小伙伴能够自行去查看一番。

序列化注意事项

一、序列化时,只对对象的状态进行保存,而无论对象的方法;

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

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

四、并不是全部的对象均可以序列化,至于为何不能够,有不少缘由了,好比:

  • 安全方面的缘由,好比一个对象拥有private,public等field,对于一个要传输的对象,好比写到文件,或者进行RMI传输等等,在序列化进行传输的过程当中,这个对象的private等域是不受保护的;
  • 资源分配方面的缘由,好比socket,thread类,若是能够序列化,进行传输或者保存,也没法对他们进行从新的资源分配,并且,也是没有必要这样实现;

五、声明为static和transient类型的成员数据不能被序列化。由于static表明类的状态,transient表明对象的临时数据。

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

  • 在某些场合,但愿类的不一样版本对序列化兼容,所以须要确保类的不一样版本具备相同的serialVersionUID;
  • 在某些场合,不但愿类的不一样版本对序列化兼容,所以须要确保类的不一样版本具备不一样的serialVersionUID。

七、Java有不少基础类已经实现了serializable接口,好比String,Vector等。可是也有一些没有实现serializable接口的;

八、若是一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存!这是能用序列化解决深拷贝的重要缘由;

总结

什么是序列化?序列化Java中经常使用实现方式有哪些?两种实现序列化方式的对比,序列化算法?如何自定义序列化?Java集合框架中序列化是如何实现的?

这几个点若是没有get到,麻烦请再次阅读,或者加我微信进群里你们一块儿聊。

相关文章
相关标签/搜索