书成

再这样堕落下去就给我去死啊你这混蛋!!!

0%

Java序列化

Serializable 接口

 序列化是指将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象 。

 在 Java 中,想要将对象序列化,需要实现 Serializable 接口启用类的可序列化性,未实现此接口的类不支持序列化与反序列化操作,如果对象某个字段不需要序列化,可以用 transient 修饰该字段。

transient 关键字

 在未实现 Serializable 接口的情况下对类进行序列化,会 java.io.NotSerializableException 异常,类在实现了 Serializable 接口后,就能启用默认的序列化机制,如果有不想被序列化的字段,可以用 transient 关键字修饰它。

1
2
3
4
5
6
public class User implements Serializable {
private static final long serialVersionUID = 369L;
private transient String name;
private int age;
// ...... 省略了其它代码
}

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws Exception{
// 序列化
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("C:\\Users\\adam\\Desktop\\user"));
User u = new User("a", 1);
out.writeObject(u);
// 反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("C:\\Users\\adam\\Desktop\\user"));
User u2 = (User) in.readObject();
System.out.println(u2);
out.close();
in.close();
}

 代码运行结果:User{name='null', age=1},可以看到,name 属性因为被标识为 transient 所以没有被序列化。

writeObject 与 readObject

 对于被 transient 标识的属性,在某些情况下如果需要序列化(例如 ArrayList ),则可以通过定义 writeObject(ObjectOutputStream s) 与 readObject(ObjectInputStream s) 自定义类的序列化,这两个方法被定义为 private 方法,但是通过单步调试可以发现,在序列化或者反序列化时,会直接运行到这两个方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User implements Serializable {
private static final long serialVersionUID = 369L;
private transient String name;
private int age;
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject() // 自动反序列化对象中没有被transient修饰的字段
this.name = (String) s.readObject();
}
private void writeObject(ObjectOutputStream s) throws IOException{
s.defaultWriteObject() // 自动序列化对象中没有被transient修饰的字段
s.writeObject(name);
}
// ...... 省略了其它代码
}

 将User类修改如上,再度运行 main 方法结果:User{name='a', age=1},可以发现,被 transient 修饰的name字段也被序列化与反序列化了。在重写writeObject和readObject这两个方法时,一定要注意这两个方法的方法签名,另外写入顺序和读取顺序必须保持一致,否则会导致反序列化失败。

serialVersionUID

 序列化运行时与每个可序列化类关联一个版本号,称为serialVersionUID,该版本号在反序列化期间用于验证序列化对象的发送方和接收方是否为该对象加载了与序列化兼容的类。如果接收方为具有不同于相应发送方的类的serialVersionUID的对象加载了一个类,那么反序列化将导致InvalidClassException。一个可序列化的类可以通过声明一个名为“serialVersionUID”的字段显式地声明它自己的serialVersionUID,该字段必须是静态的、final的和long类型的

 如果对上面的 User 类先执行序列化代码,修改它的 serialVersionUID 后再执行反序列化代码,反序列化会失败,并抛出异常:Exception in thread "main" java.io.InvalidClassException: serializedemo.User; local class incompatible: stream classdesc serialVersionUID = 369, local class serialVersionUID = 370

 另外,如果类中有包含有非基础数据类型的字段,该字段的类也应该实现 Serializable 接口,否则不能序列化。

writeReplace() 与 readResolve()

 定义 writeReplace() 方法后,当将对象序列化时,会将该方法返回的 Object 替代序列化对象。

 定义 readResolve() 方法后,当将对象反序列化时,会将该方法返回的 Object 替代反序列化对象。

1
2
3
4
5
6
7
8
9
public class User implements Serializable {
private static final long serialVersionUID = 369L;
private transient String name;
private int age;
public Object readResolve() throws ObjectStreamException{
return new User("resolve", 666);
}
// ...... 省略了其它代码
}

 比如将User类修改如上,再度运行 main 方法结果:User{name='resolve', age=666},可以发现,结果已经被替换掉了,这个方法可以用在一些单例模式里保证对象单例。

readObjectNoData

 如果序列化流没有将给定的类列为被反序列化的对象的超类,readObjectNoData方法负责初始化其特定类的对象的状态。当接收方使用反序列化实例的类的版本与发送方不同,并且接收方的版本扩展了未被发送方版本扩展的类时,可能会发生这种情况。

 对上面的User类先执行序列化,然后让User类继承一个父类A,并且在 A 中实现 readObjectNoData 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class A implements Serializable {
protected int id;
private void readObjectNoData() throws ObjectStreamException {
this.id = 100;
}
}

public class User extends A implements Serializable {

private static final long serialVersionUID = 370L;
private transient String name;
private int age;
// ......省略其它代码
}

 之后再执行反序列化,结果:User{name='a', age=1, id=100},可以看到,对于序列化时没有继承的父类,在反序列化时自动调用了它的 readObjectNoData 方法,可以设置父类属性的值。

Externalizable 接口

 该接口是 Serializable 接口的子接口,声明了两个方法 void writeExternal(ObjectOutput out) throws IOException;void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;,通过实现这两个方法可以自定义类的序列化与反序列化。