序列化使用起来比价方便,但有一些常见的细节需要注意,比如说定义 serialVersionUID 值,关键字 transient 的用法,下面就用例子来说明
定义一个bean,实现序列化的接口,
public class Student implements Serializable {
int age;
String address;
public Student(int age, String address) {
this.age = age;
this.address = address;
}
}
在main中执行序列化写入本地的方法
static final String PATH = "e:/data.txt";
public static void main(String[] args) throws Exception {
write();
}
private static void write() throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
PATH));
Student student = new Student(25, "中国");
oos.writeObject(student);
oos.close();
}
运行过后,发现电脑E盘多了个文本文件,打开txt文本,里面内容为 sr -com.example.cn.desigin.utils.JavaTest$Student I ageL addresstLjava/lang/String;xp t 涓浗,说明把对象以字节流的形式存在了文本中。我们再反序列化一下,看看能否还原成对象,执行以下代码
private static void read() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
PATH));
Student student = (Student) ois.readObject();
System.out.println("age=" + student.age + ";address=" + student.address);
ois.close();
}
打印出的内容为 age=25;address=中国,说明反序列化成功。这样写,看似没问题,实际上有隐患。如果我们的 Student 类,以后不会做任何属性的扩展,也不会在里面添加空格之类的,总之就是不会再去修改这个类,连个空格都不加之类的,那么可以这样写;如果不敢保证,比如说肯能再扩展一个 性别 的属性,那么一旦 Student 的类变化了,E盘中txt文本内容反序列化的时候,就会出错了。那么怎么办呢?这时候 serialVersionUID 就登场了,我们在 Student 中声明它就可以了,private static final long serialVersionUID = 1L; 或者让系统自动生成它的值,在我的电脑上是 private static final long serialVersionUID = 6392945738859063583L;
public class Student implements Serializable {
private static final long serialVersionUID = 6392945738859063583L;
int age;
String address;
int sex;
public Student(int age, String address, int sex) {
this.age = age;
this.address = address;
this.sex = sex;
}
}
如此,序列化文本中没有这个属性的值时,反序列化以后,值时默认值,String 类型为 null, int 类型为 0 ,依次类推。
默认的序列化会把所有属性全都记录到文本中,如果说Student中,如果我们不想把 address 属性序列化怎么办?一种方法是保存字符串,把对象通过 Gson 等第三方工具类把对象转换为json 类型的字符串,然后把 address 属性及对应的值删掉,json串支持删除节点的功能,然后保存字符串,使用的时候取出字符串,然后再通过 Gson 转换为对象。这种方法繁琐但比较保险,它支持对象Student 的包名字的变换及类名的变化,缺点是比较繁琐,总之如果你的bean对象经常变化包名的话,这是一个不错的方法,如果bean是万年位置不变的话,可以用第二种方法。第二种方法就是序列化提供的关键字 transient ,哪个属性不需要被序列化就用它来修饰即可,比如
public class Student implements Serializable {
private static final long serialVersionUID = 6392945738859063583L;
int age;
transient String address;
public Student(int age, String address) {
this.age = age;
this.address = address;
}
}
序列化文本内容为 sr -com.example.cn.desigin.utils.JavaTest$StudentX窷G9? I agexp ,反序列化以后,打印对象值为 age=25;address=null, 如此,证明此方案可以。以上是默认的序列化,即系统给咱们默认的道路,按照这条路走就可以了。如果你不想走常规路,或者默认的路满足不了你们公司的需求,那么可以自定义序列化格式,形成自己的定制版。想自己定制,成为自己的定制版,那么只需要编写 writeObject 和 readObject 方法即可,还以 Student 为例,如下
public class Student implements Serializable {
private static final long serialVersionUID = 6392945738859063583L;
int age;
String address;
public Student(int age, String address) {
this.age = age;
this.address = address;
}
//JAVA BEAN自定义的writeObject方法
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeInt(age);
out.writeObject(address);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
this.age = in.readInt();
this.address = in.readObject().toString();
}
}
运行后,保存到本地的序列化的值为 sr -com.example.cn.desigin.utils.JavaTest$StudentX窷G9? I ageL addresst Ljava/lang/String;xpw t 涓浗x, 反序列后打印的对象的值为 age=25;address=中国。 如果只想序列化 age 属性,那么不要把 address 写入即可, 把 out.writeObject(address); 和 this.address = in.readObject().toString(); 这两行代码注释掉即可,序列化的值为 sr -com.example.cn.desigin.utils.JavaTest$StudentX窷G9? I ageL addresst Ljava/lang/String;xpw x, 反序列化对象值为 age=25;address=null,这是第三种不想序列化某个属性的方法,定制版。
使用定制版需要注意些事项,writeObject 和 readObject 方法中, write 和 read 对象属性时,一定要对上顺序,顺序不能错乱,否则就错了。
下面稍微讲一下原理,我们发现,Student 对象的父类是 Object,里面没有 writeObject(ObjectOutputStream out)方法,那么Student 中的这个方法就不是重写了,怎么回事呢?一步步看吧, 我们调用 oos.writeObject(student); 方法,看一下源码
public final void writeObject(Object object) throws IOException {
writeObject(object, false);
}
这个方法,会调用 writeObjectInternal(object, unshared, true, true); 方法,把 student 引用继续往下传, 这个方法有两行比较关键的代码,
Class> objClass = object.getClass();
ObjectStreamClass clDesc = ObjectStreamClass.lookupStreamClass(objClass);
看看静态方法 ,里面用到了Map缓存技术,
static ObjectStreamClass lookupStreamClass(Class> cl) {
WeakHashMap
ObjectStreamClass cachedValue = tlc.get(cl);
if (cachedValue == null) {
cachedValue = createClassDesc(cl);
tlc.put(cl, cachedValue);
}
return cachedValue;
}
看一下 createClassDesc(cl) 方法中的关键代码
private static ObjectStreamClass createClassDesc(Class> cl) {
ObjectStreamClass result = new ObjectStreamClass();
result.methodWriteReplace = findMethod(cl, "writeReplace");
result.methodReadResolve = findMethod(cl, "readResolve");
result.methodWriteObject = findPrivateMethod(cl, "writeObject", WRITE_PARAM_TYPES);
result.methodReadObject = findPrivateMethod(cl, "readObject", READ_PARAM_TYPES);
result.methodReadObjectNoData = findPrivateMethod(cl, "readObjectNoData", EmptyArray.CLASS);
if (result.hasMethodWriteObject()) {
flags |= ObjectStreamConstants.SC_WRITE_METHOD;
}
result.setFlags(flags);
return result;
}
可看到这就明白了,原来是通过反射来检查 bean 中是否有重写这几个方法,通过反射来调用方法,所以自定义序列化时,我们自己写这两个方法,而不是重写,因为父类没有。
细心的同学会发现,下面的方法有所不同,
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age);
out.writeObject(address);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.age = in.readInt();
this.address = in.readObject().toString();
}
序列化时,多了个 out.defaultWriteObject(); 方法, 反序列化时,多了个 in.defaultReadObject(); 方法,那么这两个方法是干嘛用的呢?很明显,它们俩是对应着的,在下对这一块也不是很了解,按照个人的体会,这两个方法是相对的,要么都存在,要么都不存在; defaultWriteObject() 和 defaultReadObject() 是系统默认的序列化, out.writeInt(age); out.writeObject(address);这个是自己自定义的,可以理解为 他们是 父类和子类 方法中的关系,相同于实现父类方法同时,又扩展了子类的方法,如果 defaultWriteObject() 和自定义序列化中同时操作了 age的值,例如
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age + 10);
out.writeObject(address);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.age = in.readInt();
this.address = in.readObject().toString();
this.age = age - 1;
}
按照 Student student = new Student(23, "中国"); oos.writeObject(student); 此时,自定义为准,比如传入的age是23,out.defaultWriteObject();对应的就是23,但我们自定义时,把age的值增加了10,变为33,然后序列化,此时序列化本地文本中的值是 33; 然后反序列化时, in.defaultReadObject(); 读出来的是 33 ,在下面有减去了1,即置为 32。
运行结果, 是 age:32 address:中国 。