问:说说你对 Java 的 transient 关键字理解?
答:对于不需要被序列化的属性就可以通过加上 transient 关键字来处理。一旦属性被 transient 修饰就不再是对象持久化的一部分,该属性的内容在序列化后将无法获得访问,transient 关键字只能修饰属性变量成员而不能修饰方法和类(注意局部变量是不能被 transient 关键字修饰的),属性成员如果是引用类型也需要保证实现 Serializable 接口;此外在 Java 中对象的序列化可以通过实现两种接口来实现,若实现的是 Serializable 接口则所有的序列化将会自动进行,若实现的是 Externalizable 接口则没有任何东西可以自动序列化,需要在 writeExternal 方法中进行手工指定所要序列化的变量,这与是否被 transient 修饰无关。
问:对于 transient 修饰的属性如何在不删除修饰符的情况下让其可以序列化?
答:本题其实就是在考察实现 Serializable 接口情况下通过 writeObject() 与 readObject()方法进行自定义序列化的机制。具体实现如下:
public class Item {
public String name;
public String id;
public School() {
}
public School(String name, String id) {
this.name = name;
this.id = id;
}
}
public class Info implements Serializable {
...
transient private Item item = null;
...
private void writeObject(ObjectOutputStream out) throws IOException {
//invoke default serialization method
out.defaultWriteObject();
if (item == null) {
Item = new Item();
}
out.writeObject(item.name);
out.writeObject(item.id);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
//invoke default serialization method
in.defaultReadObject();
String name = (String) in.readObject();
String id = (String) in.readObject();
item = new Item(name, id);
}
}
上面在 writeObject() 方法中先调用了 ObjectOutputStream 的 defaultWriteObject() 方法,该方法会执行默认的序列化机制(忽略 item 字段),然后再调用 writeXXX() 方法显示地将每个字段写入到 ObjectOutputStream 中;readObject() 方法的作用是对象的读取,其原理与 writeObject() 方法相同。必须要注意的是 writeObject() 与 readObject() 都是 private 方法,其在 ObjectOutputStream 的 writeSerialData() 方法和 ObjectInputStream 的 readSerialData() 方法中通过反射进行调用。
问:简单说说 Externalizable 与 Serializable 有什么区别?
答:使用 transient 还是用 writeObject() 和 readObject() 方法都是基于 Serializable 接口的序列化;JDK 提供的另一个序列化接口 Externalizable 继承自 Serializable,使用该接口后基于 Serializable 接口的序列化机制就会失效(包括 transient,因为 Externalizable 不会主动序列化),当使用该接口时序列化的细节需要由我们自己去实现,另外使用 Externalizable 主动进行序列化时当读取对象时会调用被序列化类的无参构方法去创建一个新的对象,然后再将被保存对象的字段值分别填充到新对象中,所以实现 Externalizable 接口的类必须提供一个无参 public 的构造方法。关于 Externalizable 的实例如下:
public class Info implements Externalizable {
private String name;
private int age;
public Info() {
}//必须定义无参构造方法
public Info(String name, int age) {
this.name = name;
this.age = age;
} //实现此方法反序列化时使用
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = (String) in.readObject();
this.age = in.readInt();
} //实现此方法序列化时使用
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(this.name);
out.writeInt(this.age);
}
}
特别注意使用 Externalizable 方式时必须提供无参构造方法,且 readExternal 方法必须按照与 writeExternal 方法写入值时相同的顺序和类型来读取属性值。
问:Serializable 序列化中自定义 readObjectNoData() 方法有什么作用?
答:这个方法主要用来保证通过继承扩容后对老版本的兼容性,适用场景如下:比如类 Person 被序列化到硬盘后存储为文件 old.txt,接着 Person 被修改继承自 Animal,为了保证用新的 Person 反序列化老版本 old.txt 文件且 Animal 类的成员有默认值则可以在 Animal 类中定义 readObjectNoData 方法返回成员的默认值,具体可以参见 ObjectInputStream 类中的 readSerialData 方法判断。
问:Java 序列化中 writeReplace() 方法有什么作用?
答:Serializable 除了提供 writeObject 和 readObject 标记方法外还提供了另外两个标记方法可以实现序列化对象的替换(即 writeReplace 和 readResolve),序列化类一旦实现了 writeReplace 方法后则在序列化时就会先调用 writeReplace 方法将当前对象替换成另一个对象(该方法会返回替换后的对象),接着系统将再次调用另一个对象的 writeReplace 方法,直到该方法不再返回另一个对象为止,程序最后将调用该对象的 writeObject() 方法来保存该对象的状态。通过下面例子可以说明上面这段话(AdapterBean 只是用来说明问题,实际应用中可能是转为 Map 或者列表等其他结构):
class AdapterBean implements Serializable {
private String name;
private int age;
public AdapterBean(String name, int age) {
this.name = name;
this.age = age;
}
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
System.out.println("AdapterBean writeObject.");
}
private void readObject(java.io.ObjectInputStream in) throws Exception {
in.defaultReadObject();
System.out.println("AdapterBean readObject.");
}
}
class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
System.out.println("Person writeObject.");
}
private void readObject(java.io.ObjectInputStream in) throws Exception {
in.defaultReadObject();
System.out.println("Person readObject.");
}
private Object writeReplace() throws ObjectStreamException {
System.out.println("Person writeReplace.");
return new AdapterBean(name, age);
}
}
public class Main {
public static void main(String[] args) throws IOException, Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.serial"));
Person p = new Person("工匠若水", 27);
oos.writeObject(p);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.serial"));
System.out.println(((AdapterBean) ois.readObject()).toString());
}
}
上面程序的输出结果如下:
Person writeReplace.
AdapterBean writeObject.
AdapterBean readObject.
AdapterBean@24459efb
特别说明,实现了 writeReplace 的序列化类就不要再实现 writeObject 了,因为该类的 writeObject 方法就不会被调用了;实现 writeReplace 的返回对象必须是可序列话的对象;通过 writeReplace 序列化替换的对象在反序列化中无论实现哪个方法都是无法恢复原对象的(即对象被彻底替换了),也就是说使用 ObjectInputStream 读取的对象只能是被替换后的对象,要想恢复只能在读取后自己手动构造恢复;所以 writeObject 只和 readObject 配合使用,一旦实现了 writeReplace 在写入时进行替换就不再需要 writeObject 和 readObject 了,故替换就是彻底的自定义了,比 writeObject 和 readObject 自定义更彻底。
问:Java 序列化中 readResolve() 方法有什么作用?
答:同上 Serializable 除过提供了 writeObject 和 readObject 标记方法外还提供了另外两个标记方法可以实现序列化对象的替换(即 writeReplace 和 readResolve),readResolve() 方法可以实现保护性复制整个对象,紧挨着序列化类实现的 readObject() 之后被调用,该方法的返回值会代替原来反序列化的对象,而原来序列化类中 readObject() 反序列化的对象将会立即丢弃。readObject() 方法在序列化单例类时尤其有用,单例序列化都应该提供 readResolve() 方法,这样才可以保证反序列化的对象依然正常。同理给个直观例子如下:
final class Singleton implements Serializable {
private Singleton() {
}
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
}
public class Main {
public static void main(String[] args) throws IOException, Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.serial"));
Singleton p = Singleton.getInstance();
oos.writeObject(p);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.serial"));
Singleton p1 = (Singleton) ois.readObject();
System.out.println(p == p1);
}
}
上面代码运行结果为 true,如果去掉 readResolve 实现则结果为 false。
所以 readResolve 方法可以保护性恢复对象(同时也可以替换对象),调用该方法之前会先调用序列化类的 readObject 反序列化得到对象,在该方法中可以正常通过 this 访问到刚才反序列化得到的对象内容,然后可以根据这些内容进行一定处理返回一个对象,所以其最重要的应用就是保护性恢复单例对象(当然使用枚举类的单例就天生支持此特性)。
问:Java 序列化存储传输为什么不安全?怎么解决?
答:因为序列化二进制格式完全编写在文档中且完全可逆,所以只需将二进制序列化流的内容转储到控制台就可以看清类及其包含的内容,故序列化对象中的任何 private 字段几乎都是以明文的方式出现在序列化流中,如果我们传输的序列化数据中途被截获,截获方通过反序列化就可以获得里面的数据(敏感数据的泄露),甚至对里面的数据进行修改然后发送给接收方(无法确保数据来源的安全性),从而产生了序列化安全问题。
要解决序列化安全问题的核心原理就是避免在序列化中传递敏感数据,所以可以使用关键字 transient 修饰敏感数据的变量,或者通过自定义序列化相关流程对数据进行签名加密机制再存储或者传输(最简单譬如女生年龄可以在序列化时进行移位操作,反序列化时进行反向移位复原操作,或者使用一些加密算法处理)。