本文正在参与「金石计划 . 分割6万现金大奖」

序列化是一种目标耐久化的手段。遍及应用在网络传输、RMI等场景中。本文经过分析ArrayList的序列化来介绍Java序列化的相关内容。首要涉及到以下几个问题:

怎样完成Java的序列化

为什么完成了java.io.Serializable接口才干被序列化

transient的效果是什么

怎样自界说序列化战略

自界说的序列化战略是怎样被调用的

ArrayList对序列化的完成有什么好处

Java目标的序列化

Java渠道答应咱们在内存中创立可复用的Java目标,但一般情况下,只有当JVM处于运行时,这些目标才或许存在,即,这些目标的生命周期不会比JVM的生命周期更长。但在现实应用中,就或许要求在JVM停止运行之后能够保存(耐久化)指定的目标,并在将来重新读取被保存的目标。Java目标序列化就能够协助咱们完成该功能。

运用Java目标序列化,在保存目标时,会把其状况保存为一组字节,在未来,再将这些字节组装成目标。有必要留意地是,目标序列化保存的是目标的”状况”,即它的成员变量。由此可知,目标序列化不会关注类中的静态变量

除了在耐久化目标时会用到目标序列化之外,当运用RMI(长途办法调用),或在网络中传递目标时,都会用到目标序列化。Java序列化API为处理目标序列化提供了一个规范机制,该API简单易用。

怎样对Java目标进行序列化与反序列化

在Java中,只要一个类完成了java.io.Serializable接口,那么它就能够被序列化。这里先来一段代码:

code 1 创立一个User类,用于序列化及反序列化

package com.hollis;
import java.io.Serializable;
import java.util.Date;
/**
 * Created by hollis on 16/2/2.
 */
public class User implements Serializable{
    private String name;
    private int age;
    private Date birthday;
    private transient String gender;
    private static final long serialVersionUID = -6849794470754667710L;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public Date getBirthday() {
        return birthday;
    }
    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }
    public String getGender() {
        return gender;
    }
    public void setGender(String gender) {
        this.gender = gender;
    }
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + ''' +
                ", age=" + age +
                ", gender=" + gender +
                ", birthday=" + birthday +
                '}';
    }
}

code 2 对User进行序列化及反序列化的Demo

package com.hollis;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.io.*;
import java.util.Date;
/**
 * Created by hollis on 16/2/2.
 */
public class SerializableDemo {
    public static void main(String[] args) {
        //Initializes The Object
        User user = new User();
        user.setName("hollis");
        user.setGender("male");
        user.setAge(23);
        user.setBirthday(new Date());
        System.out.println(user);
        //Write Obj to File
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
            oos.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(oos);
        }
        //Read Obj from File
        File file = new File("tempFile");
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(file));
            User newUser = (User) ois.readObject();
            System.out.println(newUser);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(ois);
            try {
                FileUtils.forceDelete(file);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
//output 
//User{name='hollis', age=23, gender=male, birthday=Tue Feb 02 17:37:38 CST 2016}
//User{name='hollis', age=23, gender=null, birthday=Tue Feb 02 17:37:38 CST 2016}

序列化及反序列化相关常识

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的序列化

在介绍ArrayList序列化之前,先来考虑一个问题:

怎样自界说的序列化和反序列化战略

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

code 3

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接口,那么咱们就能够对它进行序列化及反序列化。由于elementData是transient的,所以咱们以为这个成员变量不会被序列化而保留下来。咱们写一个Demo,验证一下咱们的主意:

code 4

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其实便是用来保存列表中的元素的。经过该属性的声明办法咱们知道,他是无法经过序列化耐久化下来的。那么为什么code 4的结果却经过序列化和反序列化把List中的元素保留下来了呢?

writeObject和readObject办法

在ArrayList中界说了来个办法: writeObjectreadObject

这里先给出定论:

在序列化进程中,假如被序列化的类中界说了writeObject 和 readObject 办法,虚拟机会企图调用目标类里的 writeObject 和 readObject 办法,进行用户自界说的序列化和反序列化。

假如没有这样的办法,则默许调用是 ObjectOutputStream 的 defaultWriteObject 办法以及 ObjectInputStream 的 defaultReadObject 办法。

用户自界说的 writeObject 和 readObject 办法能够答应用户操控序列化的进程,比方能够在序列化的进程中动态改动序列化的数值。

来看一下这两个办法的具体完成:

code 5

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

code 6

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

那么为什么ArrayList要用这种办法来完成序列化呢?

why transient

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

why writeObject and readObject

前面说过,为了避免一个包括大量空目标的数组被序列化,为了优化存储,所以,ArrayList运用transient来声明elementData。 可是,作为一个集合,在序列化进程中还有必要确保其间的元素能够被耐久化下来,所以,经过重写writeObjectreadObject办法的办法把其间的元素保留下来。

writeObject办法把elementData数组中的元素遍历的保存到输出流(ObjectOutputStream)中。

readObject办法从输入流(ObjectInputStream)中读出目标并保存赋值到elementData数组中。

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

怎样自界说的序列化和反序列化战略

答:能够经过在被序列化的类中添加writeObject 和 readObject办法。那么问题又来了:

虽然ArrayList中写了writeObject 和 readObject 办法,可是这两个办法并没有显现的被调用啊。

那么假如一个类中包括writeObject 和 readObject 办法,那么这两个办法是怎样被调用的呢?

ObjectOutputStream

从code 4中,咱们能够看出,目标的序列化进程经过ObjectOutputStream和ObjectInputputStream来完成的,那么带着刚刚的问题,咱们来分析一下ArrayList中的writeObject 和 readObject 办法到底是怎样被调用的呢?

为了节约篇幅,这里给出ObjectOutputStream的writeObject的调用栈:

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(obj, new Object[]{ out });是要害,经过反射的办法调用writeObjectMethod办法。官方是这么解说这个writeObjectMethod的:

class-defined writeObject method, or null if none

在咱们的例子中,这个办法便是咱们在ArrayList中界说的writeObject办法。经过反射的办法被调用了。

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

假如一个类中包括writeObject 和 readObject 办法,那么这两个办法是怎样被调用的?

答:在运用ObjectOutputStream的writeObject办法和ObjectInputStream的readObject办法时,会经过反射的办法调用。

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

Serializable分明便是一个空的接口,它是怎样确保只有完成了该接口的办法才干进行序列化与反序列化的呢?

Serializable接口的界说:

public interface Serializable {
}

读者能够测验把code 1中的继承Serializable的代码去掉,再履行code 2,会抛出java.io.NotSerializableException

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

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、Array和Serializable类型,假如不是则直接抛出NotSerializableException

总结

1、假如一个类想被序列化,需求完成Serializable接口。否则将抛出NotSerializableException异常,这是由于,在序列化操作进程中会对类型进行检查,要求被序列化的类有必要属于Enum、Array和Serializable类型其间的任何一种。

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

3、在类中添加writeObject 和 readObject 办法能够完成自界说序列化战略