zoukankan      html  css  js  c++  java
  • 【Java】详解java对象的序列化

    目录结构:

    contents structure [+]

    1.序列化的含义和意义

    序列化机制允许将实现序列化的Java对象转化为字节序列,这些字节序列可以保存到磁盘上,或通过网络传输,以备以后重新恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而单独存在。如果需要让某个类支持序列化机制,那么必须实现Serializable或Externalizable接口。在Java库中,已经有许多类实现了Serializable接口,Serializable是一个标记接口,该接口无需实现任何方法,它只是表明该类是可序列化的。所有可能在网络上传输的对象的类都应该是可序列化的,否则程序将会出现异常,比如RMI(Remote Method Invoke,远程方法调用)过程中的参数和返回值;所有需要保持到磁盘里的对象的类都必须是可序列化的。

    使用java序列化的时候需要注意:
    a.对象的类名,实例变量都会被序列化;方法,类变量,transient实例变量不会被序列化。
    b.实现Serializable接口的类如果需要让某个实例变量不会序列化,则可在实例变量前加transient修饰符,而不是static关键字。虽然static关键字可以达到效果,但是static关键字不是这样用的。
    c.保证序列化对象的实例变量也是可序列化的,否则需要使用transient关键字来修饰该变量。
    d.反序列化对象时,必须有序列化对象的class文件。
    e.通过网络、文件来传输序列化的对象时候,必须按照实际写入的顺序读取。

    2.使用对象流实现序列化

    在上面我们已经了解到,如果需要将某个对象保持到磁盘或通过网络传输,那么该类该类应该实现Serializable接口或Externalizable接口。
    为了演示,首先定义一个Person类:

    public class Person implements Serializable{
        public String name;
        public int age;
        public Person(){
            System.out.println("Person的无参构造器");
        }
        public Person(String name,int age)
        {
            this.name=name;
            this.age=age;
        }
        public String toString(){
            return "name:"+name+",age:"+age;
        }
        @Override
        protected Object clone() throws CloneNotSupportedException {
            System.out.println("Person的clone方法里");
            return super.clone();
        }
    }

    下面的代码将Person对象保存到磁盘person.out文件中:

    public class WriteObject {
        public static void main(String[] args) throws Exception{
            Person person=new Person("孙悟空",500);
            FileOutputStream fos=new FileOutputStream(new File("person.out"));
            ObjectOutputStream oos=new ObjectOutputStream(fos);
            oos.writeObject(person);
        }
    }

    接下来的代码将从person.out文件中,反序列化一个Person对象:

    public class ReadObject {
        public static void main(String[] args) throws Exception{
            FileInputStream fis=new FileInputStream(new File("person.out"));
            ObjectInputStream ois=new ObjectInputStream(fis);
            Object obj=ois.readObject();
            System.out.println(obj.getClass().getSimpleName());
        }
    }

    可以看见控制台打印了

    Person

    观察上面反序列化和Person的代码。可以看出,通过实现Serializable接口序列化后,再反序列化重新构建对象是没有经过Person类的无参构造器和clone方法的。这里可以暂时理解为一种特殊创建对象的方式。

    3.对象引用的序列化

    在上面的案例中,我们定义的Person类有两个,name和age,分别为String和int类型。通过观察String可以发现String实现了Serializable接口。其实只要一个类里面有引用类型,那么这个引用类型也必须可序列化,否则拥有该类型成员变量的类也不能序列化。
    例如,在下面定义了一个Teacher类,并且持有List<Person>的引用:

    public class Teacher implements Serializable{
        public String name;
        public List<Person> students=null;
        public Teacher(String name,List<Person> students)
        {
            this.name=name;
            this.students=students;
        }
    }

    上面的类成员变量List<Person> students,其中List、Person和Person类中的引用类型成员变量都实现了Serializable接口,倘若有一个没有实现Serializable接口,那么这个Teacher都不能被成功序列化。
    我们创建了三个对象students,teacher,teacher2:

            List<Person> students=new ArrayList<Person>();
            students.add(new Person("孙悟空",500));
            students.add(new Person("猪八戒",400));
            students.add(new Person("沙僧",300));
            
            Teacher teacher1=new Teacher("唐僧",students);//引用List<Person>
            Teacher teacher2=new Teacher("玄奘",students);//引用List<Person>

    在这里需要注意的是,我们依次把这三个对象序列化到本地文件中,那么是否会得到三个不同的List<Person>对象呢,倘若是,那么teacher1和teacher2引用的对象就不是同一个对象了,显然违背了java序列化机制的初衷了。在java中,进行了特殊处理,同一个对象只会被序列化一次。
    我们使用如下代码来进行验证:

    public class WriteReadTest {
        public static void main(String[] args) throws Exception {
            List<Person> students=new ArrayList<Person>();
            students.add(new Person("孙悟空",500));
            students.add(new Person("猪八戒",400));
            students.add(new Person("沙僧",300));
            
            Teacher teacher1=new Teacher("唐僧",students);
            Teacher teacher2=new Teacher("玄奘",students);
            //开始序列化对象
            FileOutputStream fos=new FileOutputStream(new File("person.out"));
            ObjectOutputStream oos=new ObjectOutputStream(fos);
            oos.writeObject(students);
            oos.writeObject(teacher1);
            oos.writeObject(teacher2);
            //开始反序列化对象
            FileInputStream fis=new FileInputStream(new File("person.out"));
            ObjectInputStream ois=new ObjectInputStream(fis);
            List<Person> s=(List<Person>)ois.readObject();
            Teacher t1=(Teacher)ois.readObject();
            Teacher t2=(Teacher)ois.readObject();
            
            System.out.println(s==t1.students);//true
            System.out.println(s==t2.students);//true
            System.out.println(t1==t2);//false
        }
    }

    可以看出反序列化出来的三个List<Person>对象都是同一个对象。

    java的序列化机制采用了如下的算法:

    a.所有保存到磁盘中的对象都有一个序列化编号
    b.当程序试图序列化一个对象时,程序将检查该对象是否被序列化过,只有该对象从未被序列化(在本次虚拟机中)过,系统才会将该对象转化为字节序列输出。
    c.如果某个对象已经被序列化过了,程序只是输出一个序列化编号,而不是重新序列化该对象。


    通过上面的算法,我们可以得出一个结论,倘若有一个对象被多次序列化,那么只有第一次会成功被序列化,其它几次只是输出序列化编号而已,例如:
    可以用如下图来进行进一步理解:

    4.自定义序列化

    实现自定义序列化,可以通过实现Serializable或Externalizable接口。

    4.1 采用实现Serializable接口实现序列化

    通过实现Serializable接口来实现序列化是比较常用的,Serializable是一个标记性接口,实现该接口不需要做多余的额外工作。
    接下来会介绍一些常见的操作和注意事项,
    在一些特殊情况下,一个类里包含的实例变量是敏感信息,不希望被序列化到本地,那么可以在此变量上使用transient关键字。
    例如:

    public class Person implements Serializable{
        public transient String name;//transient表明 不允许被序列化到本地
        public int age;
        public Person(String name,int age)
        {
            this.name=name;
            this.age=age;
        }
    }

    测试:

            Person p1=new Person("孙悟空",500);
            //开始序列化对象
            FileOutputStream fos=new FileOutputStream(new File("person.out"));
            ObjectOutputStream oos=new ObjectOutputStream(fos);
            oos.writeObject(new Person("孙悟空",500));
            //开始反序列化对象
            FileInputStream fis=new FileInputStream(new File("person.out"));
            ObjectInputStream ois=new ObjectInputStream(fis);
            Person person1=(Person)ois.readObject();
            System.out.println("name:"+person1.name+",age:"+person1.age);//name:null,age:500

    可以在需要被序列化的列中,定义writeReplace()方法。JVM在进行序列化时候,如果未定义该方法,则不会进行序列化,如果定义了该方法,那么该方法在writeObject之后调用。
    writeObject方法的完整签名格式为:

    Object writeReplace() throws ObjectStreamException

    writeReplace在writeObject之后调用,一旦定义了writeReplace方法,那么由writeObject序列化的对象,会完全丢弃,程序被序列化的对象是writeReplace所返回的。
    通过这个特点,我们可以把被序列化的对象,替换为我们想要的任意类型:
    例如:

    public class Person implements Serializable{
        public String name;
        public int age;
        public Person(String name,int age)
        {
            this.name=name;
            this.age=age;
        }
        Object writeReplace() throws ObjectStreamException
        {
            List<Object> list=new ArrayList<Object>();
            list.add(name);
            list.add(age);
            return list;//返回了一个List<Object>类型的数据
        }
    }

    我们看到writeReplace方法返回了一个List<Object>类型的数据。

        public static void main(String[] args) throws Exception {
            Person p1=new Person("孙悟空",500);
            //开始序列化对象
            FileOutputStream fos=new FileOutputStream(new File("person.out"));
            ObjectOutputStream oos=new ObjectOutputStream(fos);
            oos.writeObject(p1);
            //开始反序列化对象
            FileInputStream fis=new FileInputStream(new File("person.out"));
            ObjectInputStream ois=new ObjectInputStream(fis);
            List<Object> p=(List<Object>)ois.readObject();
            for(Object obj : p)
            {
                System.out.println(obj);
            }
        }

    和writeReplace方法相对的就是readResolve方法,readResolve方法会在readObject()方法之后调用,
    readResolve方法的完整签名:

    Object readResolve() throws ObjectStreamException

    由于readResolve方法会在readObject之后立即调用,该方法的返回值会替代原来反序列化的对象,而原来readObject反序列化的对象将会被立即抛弃。
    readResolve方法在序列化单例类、枚举类时尤其有用。如果使用java5提供的枚举,当然没问题,但如果在java5以前,那么在序列化时,就需要注意了

    public class Orientation implements Serializable{
        public static final Orientation HORIZONTAL=new Orientation(1);
        public static final Orientation VERTICAL=new Orientation(2);
        private int value;
        private Orientation(int value){
            this.value=value;
        }
    }

    这样的代码在java5以前经常用来表示枚举,如果使用如下的代码进行序列化:

           Orientation o1=Orientation.HORIZONTAL;
            //开始序列化对象
            FileOutputStream fos=new FileOutputStream(new File("person.out"));
            ObjectOutputStream oos=new ObjectOutputStream(fos);
            oos.writeObject(o1);
            //开始反序列化对象
            FileInputStream fis=new FileInputStream(new File("person.out"));
            ObjectInputStream ois=new ObjectInputStream(fis);
            Orientation o=(Orientation)ois.readObject();
            System.out.println(o==o1);//false

    会发现结果为false,这显然不是我们想要的。这是因为反序列化的对象时重新构建的对象,我们可以定义readResolve方法来解决这个问题:

    public class Orientation implements Serializable{
        public static final Orientation HORIZONTAL=new Orientation(1);
        public static final Orientation VERTICAL=new Orientation(2);
        private int value;
        private Orientation(int value){
            this.value=value;
        }
        Object readResolve() throws ObjectStreamException
        {
            if(value==1){
                return HORIZONTAL;
            }else if(value==2)
            {
                return VERTICAL;
            }else{
                return null;
            }
        }
    }

    这样一来,被序列化前后的对象就相等了。这样之所以,是因为序列化不包括静态变量,由于readObject后会立即调用readResolve方法,我们又在readResolve中返回了一个类变量,所以前后得到的是同一个对象。
    接下来附张图片,表示writeObject,writeReplace,readObject,readResolve方法间的调用前后顺序:

    4.2采用实现Externalizable接口实现序列化

    采用实现Externalizable的方式和实现Serializable的方式具有相同的效果。在上面介绍Serializable里的常用操作和方法在Externalizable接口里也同样适用,这里就不一一介绍那些操作。
    采用实现Externalizable接口的方法,必须重新Externalizable接口里的两个抽象方法writeExternal和readExternal方法。

    public class Person implements Externalizable{
        public String name;
        public int age;
        public Person(){
            System.out.println("公共无参构造器");
        }
        public Person(String name,int age)
        {
            this.name=name;
            this.age=age;
        }
        /**
         * 在序列化的时候调用
         */
        @Override
        public void writeExternal(ObjectOutput out)
                throws IOException {
            out.writeObject(name);
            out.writeInt(age);
        }
        /**
         * 在反序列化的时候调用
         */
        @Override
        public void readExternal(ObjectInput in) throws IOException,
                ClassNotFoundException {
            this.name=(String)in.readObject();
            this.age=in.readInt();
        }
    }

    测试代码为:

        public static void main(String[] args) throws Exception {
            Person p=new Person("孙悟空",500);
            //开始序列化对象
            FileOutputStream fos=new FileOutputStream(new File("person.out"));
            ObjectOutputStream oos=new ObjectOutputStream(fos);
            oos.writeObject(p);
            //开始反序列化对象
            FileInputStream fis=new FileInputStream(new File("person.out"));
            ObjectInputStream ois=new ObjectInputStream(fis);
            Person p2=(Person)ois.readObject();
            System.out.println("name:"+p2.name+",age:"+p2.age);
        }

    打印结果为:

    公共无参构造器
    name:孙悟空,age:500

    我们可以看到执行了Person的公共无参构造器,这是使用Externalizable和Serializable的不同点;使用Externalizable来反序列化时,是调用反序列化的类的公共无参构造器,然后在readExternal方法中对成员变量赋值,而Serializable是不会调用任何构造器的。

    5序列化的版本问题

    通过前面的介绍,我们知道了反序列化java对象必须提供该对象的class文件,现在的问题是,顺着项目的升级,系统的class文件也会升级,java如何保证两个class文件的兼容性?
    java序列化机制允许为序列化类提供一个private static final 的serialVersionUID值,该类变量的值用于表示该java类的序列化版本,也就是一个类升级后,只要它的serialVersionUID的值不变,序列化机制也会把他们当做同一个序列化版本。

    public class Test{
        private static final long serialVersionUID=512L;
    }

    为了在反序列化时确保序列化版本的兼容性,最好在每个要序列化的的类中加入private static final long seriaVersionUID值的类变量,具体数值自己定义。这样,计时在某个类序列化之后,它所对应的类被修改了,该对象也依然可以被正确的反序列化。

    可以通过JDK的bin目录下的serialver.exe工具来获得该类的serialVersionUID类变量的值。

    serialver Person
  • 相关阅读:
    JDK 7 和 JDK 8 的区别
    浅显了解数据库存储引擎
    C++ 字符函数
    华为机试题 合唱队
    华为机试题 密码验证合格程序
    华为机试题 删除字符串中出现次数最少的字符
    如何解决机器学习中数据不平衡问题
    三分(求单峰或单谷)
    Vim配置——自动缩进(C/C++)
    Html日期控件
  • 原文地址:https://www.cnblogs.com/HDK2016/p/6838908.html
Copyright © 2011-2022 走看看