Java对象序列化

262 阅读7分钟

为什么需要序列化

  当我们创建对象时,只要需要,对象就会一直存在,但在程序终止的时候,无论如何它都不会继续存在。这么做有一定的意义,但是如果对象能够在程序不运行的情况下仍能存在并保存其信息,这样,在下次运行程序时,该对象将被重建并且拥有的信息与在程序上次运行时它所拥有的信息相同。为达到这目的,通常的做法可以写入文件或数据库,这样有其优点,但缺乏了某些便利性,在Java中,通过序列化技术,可以将对象声明为“持久性”的,为我们处理所有细节,显得十分方便。

  Java 的对象序列化将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象,这一过程甚至可以通过网络进行,也意味着能自动弥补不同操作系统之间的差异,运行在 Windows 系统的计算机上创建一个对象并序列化,通过网络将其发送给一台运行 Unix 系统的计算机,不必担心数据在不同机器上表示不同从而在那里重新组装。

序列化的用途

  利用对象序列化可以实现轻量级持久性。”持久性“意味着一个对象的生存周期并不取决于程序是否正在执行,它可以生存于程序的调用之间。通过将一个序列化对象写入磁盘,然后重新调用程序时恢复该对象,就能够实现持久化的效果。对象序列化主要用于两个方面:

  • 远程方法调用(Remote Method Invocation,RMI),使存活于其他计算机上的对象使用起来就像是存活于本机上一样,当向远程对象发送消息时,需要通过对象序列化来传输参数和返回值
  • Java Beans。使用一个 Bean 时,一般情况下是在设计阶段对它的状态信息进行配置,这种状态信息必须保存下来,并在程序启动时进行后期恢复,这种具体工作就是由对象序列化完成的。

如何实现序列化

  要序列化一个对象,首先要创建某些OutputStream对象,然后将其封装在一个ObjectOutputStream对象内。这时,只需调用writeObject()即可将对象序列化,并将其发送给OutputStream(对象化序列是基于字节的,因要使用InputStreamOutputStream继承层次结构)。

  反向序列化(即将一个序列还原为一个对象),需要将一个InputStream封装在ObjectInputStream内,然后调用readObject()

package test;

import java.io.*;

public class SerializableDemo implements Serializable{
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        User user = new User();
        user.setUsername("hello");
        user.setPasswd("world");

        //write Obj to File
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("file"));
        oos.writeObject(user);
        oos.close();

        //read Obj from File
        File file = new File("file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        User newUser = (User)ois.readObject();
        ois.close();
        System.out.println(newUser);
    }
}

class User implements Serializable{
    private String username;
    private String passwd;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPasswd() {
        return passwd;
    }

    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }

    @Override
    public String toString() {
        return "username = " + getUsername() + "\r\n"
                + "passwd = " + getPasswd();
    }
}
/*
Output:
username = hello
passwd = world
 */

  此外,还可以通过实现Externalizable接口——代替实现Serializable接口——来对序列化进行控制。这个Externalizable接口继承了Serialazable接口,同时增添了两个方法:writeExternal()readExternal(),这两个方法会在序列化和反序列化还原的过程中被自动调用,以便执行一些特殊操作。

transient(瞬时)关键字

  当我们对序列化进行控制时,可能不想让 Java 的序列化机制自动保存与恢复某些敏感信息字段,比如密码,即使这些字段是私有属性,一经序列化处理,人们就可以通过读取文件或者拦截网络传输的方式来访问它。为了允以控制,可以用transient(瞬时)关键字逐个字段地关闭序列化。修改上述例子,将 passwd 字段设置为transient

package test;

import java.io.*;

public class SerializableDemo implements Serializable{
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        User user = new User();
        user.setUsername("hello");
        user.setPasswd("world");

        //write Obj to File
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("file"));
        oos.writeObject(user);
        oos.close();

        //read Obj from File
        File file = new File("file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        User newUser = (User)ois.readObject();
        ois.close();
        System.out.println(newUser);
    }
}

class User implements Serializable{
    private String username;
    private transient String passwd;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPasswd() {
        return passwd;
    }

    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }

    @Override
    public String toString() {
        return "username = " + getUsername() + "\r\n"
                + "passwd = " + getPasswd();
    }
}
/*
Output:
username = hello
passwd = null
 */

可以看到,passwd输出的值由 world 变为 null,此外,虽然toString()经过重载,但是 null 引用会被自动转换成字符串 null。

自定义序列化和反序列化策略

  通过创建ObjectOutputStream封装对象,然后调用writeObject()方法实现序列化,在调用ObjectOutputStream.writeObject()时,会检查所传递的Serializable对象,看看是否实现了它自己的writeObject()。如果存在自己实现的writeObject()方法,就跳过正常的序列化过程并调用它的writeObject(),同样,readObject()情形与此相同。此外,在自定义的writeObject()内部,可以调用defaultWriteObject()来选择执行默认的writeObject();类似地,在readObject()内部,可以调用defaultReadObject()

//: io/SerialCtl.java
// Controlling serialization by adding your own
// writeObject() and readObject() methods.
import java.io.*;

public class SerialCtl implements Serializable {
  private String a;
  private transient String b;
  public SerialCtl(String aa, String bb) {
    a = "Not Transient: " + aa;
    b = "Transient: " + bb;
  }
  public String toString() { return a + "\n" + b; }
  private void writeObject(ObjectOutputStream stream)
  throws IOException {
    stream.defaultWriteObject();
    stream.writeObject(b);
  }
  private void readObject(ObjectInputStream stream)
  throws IOException, ClassNotFoundException {
    stream.defaultReadObject();
    b = (String)stream.readObject();
  }
  public static void main(String[] args)
  throws IOException, ClassNotFoundException {
    SerialCtl sc = new SerialCtl("Test1", "Test2");
    System.out.println("Before:\n" + sc);
    ByteArrayOutputStream buf= new ByteArrayOutputStream();
    ObjectOutputStream o = new ObjectOutputStream(buf);
    o.writeObject(sc);
    // Now get it back:
    ObjectInputStream in = new ObjectInputStream(
      new ByteArrayInputStream(buf.toByteArray()));
    SerialCtl sc2 = (SerialCtl)in.readObject();
    System.out.println("After:\n" + sc2);
  }
} /* Output:
Before:
Not Transient: Test1
Transient: Test2
After:
Not Transient: Test1
Transient: Test2
*///:~

ArrayList的序列化

  通过阅读源码,我们知道ArrayList的底层是数组实现的,其elementData[]就是用来保存元素的,其定义如下:

transient Object[] elementData; // non-private to simplify nested class access

elementData通过transient声明,因此无法通过序列化技术保存下来,但是我们可以从一个实例中看出,其通过序列化和反序列化技术将 List 中的元素保存下来:

package test;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class ListDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        List<String>list = new ArrayList<>();
        list.add("hello");
        list.add("world");

        //write Obj to File
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("file"));
        oos.writeObject(list);
        oos.close();

        //read Obj from File
        File file = new File("file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        List<String>newList = (List<String>) ois.readObject();
        ois.close();
        System.out.println(newList);
    }
}
/*
[hello, world]
 */

再通过源码可知,其添加了writeObject()readObject()的方法来控制序列化和反序列化

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


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

那到这里,不禁会思考为什么要这么转好几个弯来实现序列化和反序列化,一开始直接不将elementData声明为transient不就行了?实际上,ArrayList通过动态数组的技术,当数组放满后,自动扩容,而在扩容的这部分假设仅用了一小部分,那么就会序列化一大部分没有元素的数组,导致浪费空间,为了保证在序列化的时候不会将这么大部分没有元素的数组进行序列化,因此设置为 transient。

写在最后

  因为在阅读 ArrayList 源码的时候,不知道 transient 关键字有何作用,因此在学习之后,以作记录,部分内容源于 《Thinking in Java》