前言
最近在看 《JAVA 编程思想》看到 I/O 这一章中有关对象序列化相关的内容之前不熟悉,于是借本文做一个整理与记录。
Serializable 接口
在 Java 中要序列化一个对象非常简单,只需要该类实现了 Serializable
接口我们便能通过 I/O 流的方式来序列化与反序列化对象。
// 一个简单的 Person 类
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void setName(String name) {this.name = name;}
public void setAge(int age) {this.age = age;}
public String getName() {return name;}
public int getAge() {return age;}
}
public class Test {
private static String outputFile = "./IOFiles/Person.obj";
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream(
new File(outputFile)));
Person person = new Person("Li Si", 20);
// 将对象序列化
out.writeObject(person);
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(
new File(outputFile)));
// 恢复序列化的对象
Person recoveredPerson = (Person)in.readObject();
System.out.println("name: " + recoveredPerson.getName() + ", age: " + recoveredPerson.getAge());
}
}
可以看到实现了 Serializable
接口就能非常方便的进行对象的序列化与反序列化,实际上 Serializable
的序列化非常强大不单单是能序列化上述这个例子中的简单属性,事实上其在序列化的过程中会将对象中的引用一起序列化而如果引用对象也具有引用就会一直追溯下去,最终会建立一个对象的全景图或者说对象网,因此实现通过这种方式的序列化的对象我们可以在反序列化后直接使用不会出现对象中的引用对象不存在而出错的问题。
Externalizable
Serializable
接口很强大能够自动的序列化对象中的所有信息,但是有些时候我们不希望这样,比如对于密码这类必要重要的属性就喜欢能够不对其进行序列化,Java 通过实现 Externalizable
接口然后在 writeExternal
与 readExternal
方法中就指定需要序列化的内容就可以避免那些我们不希望被序列化的内容被序列化,具体见下面代码:
public class Account implements Externalizable {
private String username;
private String password;
// 使用这种方式需要一个 public 的空构造器
public Account(){}
public Account(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// 希望密码保密因此我们只序列化 username
out.writeUTF(username);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
username = in.readUTF();
}
// setter and getter
}
public class Test {
private static String outputFile = "./IOFiles/Account.obj";
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream(
new File(outputFile)));
Account account = new Account("USERNAME", "PASSWORD");
// 将对象序列化
out.writeObject(account);
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(
new File(outputFile)));
// 恢复序列化的对象
Account recoveredAccount = (Account)in.readObject();
System.out.println("username: " + recoveredAccount.getUsername() +
", password: " + recoveredAccount.getPassword());
}
}
可以看到我们只序列化了 username
属性而 password
并没有被序列化,需要注意的是这种方式在恢复对象时与 Serializable
接口不同 Serializable
接口 直接从存储的二进制文件中构造对象,而 Externalizable
接口在恢复的过程中会调用默认的构造器因此我们需要显示的定义一个 public 的空构造器,还有需要注意的是我们需要在 writeExternal
方法中显示的写出我们需要序列化的属性然后在 readExternal
方法中按照相同的顺序写入反序列化的属性才能使得我们的序列化能够得到正确的结果,这种方式很像 Hadoop 中序列化数据的方式或者反过来 Hadoop 序列化数据的操作很像这种模式。
transient 关键字
上述方法虽然可以达到只序列化我们想序列化的内容的目的,但是如果类中的属性非常多那么 write 和 read 两个方法写起来还是很费力的,在只有少数我们不需要序列化的内容的情况下我们可以使用 transient
来达到相同的目的,在实现 Serializable
接口的类中如果属性被 transient
关键字修饰那么其就不会被序列化,见下面代码:
// 还是我们的 Account 类但是这次我们实现 Serializabl接口并用 transient 关键字修饰 password
public class Account implements Serializable {
private String username;
private transient String password;
public Account(){}
public Account(String username, String password) {
this.username = username;
this.password = password;
}
// setter and getter
}
public class Test {
private static String outputFile = "./IOFiles/Account.obj";
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream(
new File(outputFile)));
Account account = new Account("USERNAME", "PASSWORD");
// 将对象序列化
out.writeObject(account);
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(
new File(outputFile)));
// 恢复序列化的对象
Account recoveredAccount = (Account)in.readObject();
System.out.println("username: " + recoveredAccount.getUsername() +
", password: " + recoveredAccount.getPassword());
}
}
可以看到效果与 Externalizable
接口一样 password
也安全地没有被序列化。不过有些时候我们希望被 transient
也能被序列化那该怎么办呢,这操作感觉有些多余或者难以理解,但我们的好朋友 ArrayList
在实现的时候就是这样的,见下面代码:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
.....
// ArrayList 中实际存储数据的 elementData 数组就是被 transient 修饰的
transient Object[] elementData;
.....
}
那看到这个就会有疑问了难道我序列化 ArrayList
以后我存的数据就丢了吗,那我序列化的意义在哪呢?那数据当然不会丢这就要用到之前所说的序列化被 transient
关键字修饰的方法,具体的实现与 Externalizable
接口中的序列化方法很相似,那我们就继续用 ArrayList
中的代码吧:
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();
}
}
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
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, 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();
}
}
}
可以看到使用 write 和 read 的方式就可以将我们被 transient
修饰的关键字序列化出去了,为了到达这个目的我们在编写方法是必须严格的按照上述两个方法的方法签名即:
// 注意此处为 private
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{}
// 注意此处为 private
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {}
很奇怪的一点是这两个方法显然不属于 Serializable
接口因为它们是 private 的而接口中不能定义私有方法,而从形式上来说完成的功能又类似于实现接口,确实是很让人迷惑的设计,OK 不纠结这个问题了来解决一下既然要序列化那么为什么要用 transient
关键字修饰呢,从上面的代码就可以看到在 ArrayList
中存放数据的数组实际上会存在许多的空值,那么将那些空值都序列化是浪费资源的一种行为,而通过 transient
关键字加上这种序列化方式我们就可以只序列化那些有用的信息从而节省资源。因此当我们的属性中存在一些我们不需要的内容的时候就可以使用 transient
关键字修饰然后自己写序列化方法从而节省资源。
完。
参考
《Thinking in Java》 Bruce Eckel