经过ArrayList底层源码了解Java的序列化

序列化基础知识

    关于序列化的用法及基础知识,因为不是本文重点,这里再也不详细介绍了,这里只作简单的介绍。java

  1.  在Java中,只要一个类实现了java.io.Serializable接口,那么它就能够被序列化。
  2.  经过ObjectOutputStreamObjectInputStream对对象进行序列化及反序列化。
  3.  虚拟机是否容许反序列化,不只取决于类路径和功能代码是否一致,一个很是重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID
  4. 序列化并不保存静态变量。
  5.  要想将父类对象也序列化,就须要让父类也实现Serializable 接口。
  6. transient 关键字的做用是控制变量的序列化,在变量声明前加上该关键字,能够阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
  7. 服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,好比密码字符串等,但愿对该密码字段在序列化时,进行加密,而客户端若是拥有解密的密钥,只有在客户端进行反序列化时,才能够对密码进行读取,这样能够必定程度保证序列化对象的数据安全。

ArrayList的序列化

    为了深刻的介绍序列化,咱们这篇文章准备从Java源码中的ArrayList类入手。看看Java自身是如何使用序列化的。在介绍ArrayList序列化以前,先来考虑一个问题:数组

    问:如何自定义的序列化和反序列化策略?安全

    带着这个问题,咱们来看java.util.ArrayList的源码服务器

    code1:dom

public class ArrayList<E> extends AbstractList<E>
       implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
   private static final long serialVersionUID = 8683452581122892189L;
   transient Object[] elementData; // non-private to simplify nested class access
   private int size;
}

        笔者省略了其余成员变量,从上面的代码中能够知道ArrayList实现了java.io.Serializable接口,那么咱们就能够对它进行序列化及反序列化。由于负责保存元素的elementDatatransient的,因此咱们认为这个成员变量的内容不会被序列化而保留下来。咱们写一个Demo,验证一下咱们的想法: 优化

  code2:加密

public static void main(String[] args) throws IOException, ClassNotFoundException {
       List<String> stringList = new ArrayList<String>();
       stringList.add("hello");
       stringList.add("world");
       stringList.add("hollis");
       stringList.add("chuang");
       System.out.println("init StringList" + stringList);
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("stringlist"));
       objectOutputStream.writeObject(stringList);

       IOUtils.close(objectOutputStream);
       File file = new File("stringlist");
       ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
       List<String> newStringList = (List<String>)objectInputStream.readObject();
       IOUtils.close(objectInputStream);
       if(file.exists()){
           file.delete();
       }
       System.out.println("new StringList" + newStringList);
   }
//init StringList[hello, world, hollis, chuang]
//new StringList[hello, world, hollis, chuang]

        了解ArrayList的人都知道,ArrayList底层是经过数组实现的。那么数组elementData其实就是用来保存列表中的元素的。经过该属性的声明方式,咱们认为,他应该是没法经过序列化持久化下来的。spa

    问:为何code 2的结果却经过序列化和反序列化把List中的元素保留下来了呢?debug

writeObject和readObject方法

    在ArrayList中定义了来个方法: writeObjectreadObject。这里先给出结论:code

        在序列化过程当中,若是被序列化的类中定义了writeObject readObject 方法,虚拟机会试图调用对象类里的 writeObjectreadObject 方法,进行用户自定义的序列化和反序列化。若是没有这样的方法,则默认调用是 ObjectOutputStream defaultWriteObject 方法以及 ObjectInputStreamdefaultReadObject 方法。

        也就述说,用户自定义的 writeObject readObject 方法能够容许用户控制序列化的过程,好比能够在序列化的过程当中动态改变序列化的数值。咱们发现ArrayList中有这两个方法的实现,那么基本能够肯定,elementData能被序列化持久下来,确定和这两个方法有关,虽然他被声明为transitent,那么咱们来看一下ArrayList类中这两个方法的具体实现:

    code 3:

private void readObject(java.io.ObjectInputStream s)
       throws java.io.IOException, ClassNotFoundException {
       elementData = EMPTY_ELEMENTDATA;
       // Read in size, and any hidden stuff
       s.defaultReadObject();
       // Read in capacity
       s.readInt(); // ignored
       if (size > 0) {
           // be like clone(), allocate array based upon size not capacity
           ensureCapacityInternal(size);
           Object[] a = elementData;
           // Read in all elements in the proper order.
           for (int i=0; i<size; i++) {
               a[i] = s.readObject();
           }
       }
   }

    code4:

private void writeObject(java.io.ObjectOutputStream s)
       throws java.io.IOException{
       // Write out element count, and any hidden stuff
       int expectedModCount = modCount;
       s.defaultWriteObject();
       // Write out size as capacity for behavioural compatibility with clone()
       s.writeInt(size);
       // Write out all elements in the proper order.
       for (int i=0; i<size; i++) {
           s.writeObject(elementData[i]);
       }
       if (modCount != expectedModCount) {
           throw new ConcurrentModificationException();
       }
   }

     经过上面两段代码,咱们发现,raedObject方法和writeObjec方法中定义了关于elementData的序列化策略。如今,咱们能够回答刚刚的问题了。

    问:为何code 2的结果却经过序列化和反序列化把List中的元素保留下来了呢?

    答:ArrayList中定义了raedObjectwriteObject方法,这两个方法中定义了elementData的序列化及反序列化策略。

    那么,问题又来了。

    问:为何ArrayList要用这种方式来实现序列化呢?

    why transient

        ArrayList其实是动态数组,每次在放满之后自动增加设定的长度值,若是数组自动增加长度设为100,而实际只放了一个元素,那就会序列化99个null元素。为了保证在序列化的时候不会将这么多null同时进行序列化,ArrayList把元素数组设置为transient

    why writeObject and readObject

        前面说过,为了防止一个包含大量空对象的数组被序列化,为了优化存储,因此,ArrayList使用transient来声明elementData。 可是,做为一个集合,在序列化过程当中还必须保证其中的元素能够被持久化下来,因此,经过重写writeObject 和 readObject方法的方式把其中的元素保留下来。

writeObject方法把elementData数组中的元素遍历的保存到输出流(ObjectOutputStream)中。readObject方法从输入流(ObjectInputStream)中读出对象并保存赋值到elementData数组中。

至此,咱们先试着来回答刚刚提出的问题:

    问:为何ArrayList要用这种方式来实现序列化呢?

    答:避免elementData数组中过多的无用的null被序列化。

    问:如何自定义的序列化和反序列化策略?

    答:能够经过在被序列化的类中增长writeObjectreadObject方法。

    那么问题又来了,虽然ArrayList中写了writeObject readObject 方法,可是这两个方法并无显示的被调用啊。

    问:若是一个类中包含writeObject readObject 方法,那么这两个方法是怎么被调用的呢?

ObjectOutputStream

    从code 4中,咱们能够看出,对象的序列化过程经过ObjectOutputStreamObjectInputputStream来实现的,那么带着刚刚的问题,咱们来分析一下ArrayList中的writeObject readObject 方法究竟是如何被调用的呢?

    为了节省篇幅,这里给出ObjectOutputStreamwriteObject的调用栈:

    writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject

    这里看一下invokeWriteObject:

void invokeWriteObject(Object obj, ObjectOutputStream out)
       throws IOException, UnsupportedOperationException
   {
       if (writeObjectMethod != null) {
           try {
               writeObjectMethod.invoke(obj, new Object[]{ out });
           } catch (InvocationTargetException ex) {
               Throwable th = ex.getTargetException();
               if (th instanceof IOException) {
                   throw (IOException) th;
               } else {
                   throwMiscException(th);
               }
           } catch (IllegalAccessException ex) {
               // should not occur, as access checks have been suppressed
               throw new InternalError(ex);
           }
       } else {
           throw new UnsupportedOperationException();
       }
   }

        其中writeObjectMethod.invoke();是关键,经过反射的方式调用writeObjectMethod方法。官方是这么解释这个writeObjectMethod的:

        class-defined writeObject method, or null if none 

        在咱们的例子中,这个方法就是咱们在ArrayList中定义的writeObject方法。经过反射的方式被调用了。

至此,咱们先试着来回答刚刚提出的问题:

    问:若是一个类中包含writeObject readObject 方法,那么这两个方法是怎么被调用的?

    答:在使用ObjectOutputStreamwriteObject方法和ObjectInputStreamreadObject方法时,会经过反射的方式调用。

    至此,咱们已经介绍完了ArrayList的序列化方式。那么,不知道有没有人提出这样的疑问:

    问:Serializable明明就是一个空的接口,它是怎么保证只有实现了该接口的方法才能进行序列化与反序列化的呢?

    Serializable接口的定义:

public interface Serializable {
}

        若是尝试对一个未实现Serializable接口的类进行序列化,会抛出java.io.NotSerializableException。这是为何呢?Serializable只是一个空接口,如何实现的呢?

        其实这个问题也很好回答,咱们再回到刚刚ObjectOutputStreamwriteObject的调用栈:

        writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject

        writeObject0方法中有这么一段代码:

if (obj instanceof String) {
       writeString((String) obj, unshared);
   } else if (cl.isArray()) {
       writeArray(obj, desc, unshared);
   } else if (obj instanceof Enum) {
       writeEnum((Enum<?>) obj, desc, unshared);
   } else if (obj instanceof Serializable) {
       writeOrdinaryObject(obj, desc, unshared);
   } else {
       if (extendedDebugInfo) {
           throw new NotSerializableException(
               cl.getName() + "\n" + debugInfoStack.toString());
       } else {
           throw new NotSerializableException(cl.getName());
       }
   }

     在进行序列化操做时,会判断要被序列化的类是不是Enum、ArraySerializable类型,若是不是则直接抛出NotSerializableException

    问:Serializable明明就是一个空的接口,它是怎么保证只有实现了该接口的方法才能进行序列化与反序列化的呢?

    答:在类的序列化过程当中,会使用instanceof关键字判断一个类是否继承了Serializable类,若是没有,则直接抛出NotSerializableException异常。

总结 

    一、若是一个类想被序列化,须要实现Serializable接口。不然将抛出NotSerializableException异常,这是由于,在序列化操做过程当中会对类型进行检查,要求被序列化的类必须属于Enum、ArraySerializable类型其中的任何一种。

    二、在变量声明前加上该关键字,能够阻止该变量被序列化到文件中。

    三、在类中增长writeObject readObject 方法能够实现自定义序列化策略。

相关文章
相关标签/搜索