第七十五条 考虑使用自定义的序列化形式

序列化使用起来比价方便,但有一些常见的细节需要注意,比如说定义 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<Class<?>, ObjectStreamClass> tlc = getCache();
        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:中国 。       

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/85233150