• Java基础(五)-Java序列化与反序列化


    本文主要从以下方面记录:

    1、Java序列化和反序列化是什么?

    2、为什么需要序列化与反序列化?

    3、怎么实现Java序列化和反序列化?

    4、几个序列化注意事项

    一、Java序列化和反序列化是什么?

      通俗的来讲,序列化过程就是将对象转成二进制流存入内存或者文件,反序列化从内存或文件中读取二进制流转换成对象。

      

    二、为什么需要序列化与反序列化?

      其实这个问题也就是它们的应用场景有哪些?这样就容易回答多了。比如文件(文本、图片等)进行传输,这些文件都是通过二进制序列的形式进行传输的(序列化过程),而接收方则要读取这些二进制数据进行相对应的转换(反序列化过程)。除了这个它主要用于网络传输(进程之间的通信等)。

    三、怎么实现Java序列化和反序列化?

      要想实现序列化有个必要条件就是要实现Serializable接口或Externalizable接口。大部分可能只知道有Serializable接口没有关注Externalizable接口,那么你看了本文之后就应该知道了,后面再介绍它们的区别。

      有了上面个必要条件后还需要借助jdk中有两个类:java.io.ObjectOutputStream和java.io.ObjectInputStream,它们分别负责序列化和反序列化。我们可以看下这两个类的说明就知道是这两个类负责相对应的功能。

    /**
     * An ObjectOutputStream writes primitive data types and graphs of Java objects
     * to an OutputStream.  The objects can be read (reconstituted) using an
     * ObjectInputStream.  Persistent storage of objects can be accomplished by
     * using a file for the stream.  If the stream is a network socket stream, the
     * objects can be reconstituted on another host or in another process.
     *
     * <p>Only objects that support the java.io.Serializable interface can be
     * written to streams.  The class of each serializable object is encoded
     * including the class name and signature of the class, the values of the
     * object's fields and arrays, and the closure of any other objects referenced
     * from the initial objects.
     * ...
     */
    public class ObjectOutputStream
        extends OutputStream implements ObjectOutput, ObjectStreamConstants{}
    /**
     * An ObjectInputStream deserializes primitive data and objects previously
     * written using an ObjectOutputStream.
     *
     * <p>ObjectOutputStream and ObjectInputStream can provide an application with
     * persistent storage for graphs of objects when used with a FileOutputStream
     * and FileInputStream respectively.  ObjectInputStream is used to recover
     * those objects previously serialized. Other uses include passing objects
     * between hosts using a socket stream or for marshaling and unmarshaling
     * arguments and parameters in a remote communication system.
     * ...
     */
    public class ObjectInputStream
        extends InputStream implements ObjectInput, ObjectStreamConstants{
    }    

      下面我们以学生对象为例,将对象序列化保存至文件中,再从文件中反序列化转换成对象。

    import java.io.Serializable;
    
    /**
     * @Description: 学生类 已实现序列化接口
     * @author yuanfy
     * @date 2018年1月11日 上午11:36:37 
     * @version 1.0
     */
    public class Student implements Serializable{
    
        private static final long serialVersionUID = 6415983562512521049L;
    
        private String name;
        
        private int age;
        
        private String sex;
        
        public Student(String name, int age, String sex) {
            this.name = name;
            this.age = age;
            this.sex = sex;
        }
        
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
        public String getSex() {
            return sex;
        }
        public void setSex(String sex) {
            this.sex = sex;
        }
        @Override
        public String toString() {
            return "Student [name=" + name + ", age=" + age + ", sex=" + sex + "]";
        }
    }

      上面已经定义Java对象, 下面进行序列化测试

        @Test
        public void testSerializable() throws FileNotFoundException, IOException{
            Student s = new Student("james", 31, "man");
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("E:\Student.txt")));
            oos.writeObject(s);
            oos.close();
        }

       如果电脑E盘存在的话运行肯定通过(windows系统),单元测试通过后(说明序列化过程已完成)查看Student.txt文件中的内容,如下:

      

      上面是正常的情况, 如果Student类没有实现Serializable接口呢?那么序列化时会存在什么样的问题。我们把Student类去掉Serializable接口的实现,然后再进行序列化测试。这时你会发现单元测试后不通过,先看看报错原因:下面截图中的错误信息提示Student类没有被序列化。

      

      那么为什么java.io.ObjectOutputStream类会抛出这样的异常呢,它是怎么识别有没有实现Serializable接口的。接下来,我们来看看他的源码:writeObject方法中调用了writeObject()方法,所以主要逻辑在它里面。

        /**
         * Underlying writeObject/writeUnshared implementation.
         */
        private void writeObject0(Object obj, boolean unshared)
            throws IOException
        {
            boolean oldMode = bout.setBlockDataMode(false);
            depth++;
            try {
                //前面部分代码省略
                // remaining cases
                if (obj instanceof String) {
                    writeString((String) obj, unshared);
                } else if (cl.isArray()) {
                    writeArray(obj, desc, unshared);
                } else if (obj instanceof Enum) {
                    writeEnum((Enum<?>) obj, desc, unshared);
                } else if (obj instanceof Serializable) {//判断是否是Serializable的子类。
                    writeOrdinaryObject(obj, desc, unshared);
                } else {
                    //除了String类型、数组类型和枚举类型,其他对象如果没有实现Serializable接口,都会抛出NotSerializableException异常
                    if (extendedDebugInfo) {
                        throw new NotSerializableException(
                            cl.getName() + "
    " + debugInfoStack.toString());
                    } else {
                        throw new NotSerializableException(cl.getName());
                    }
                }
            } finally {
                depth--;
                bout.setBlockDataMode(oldMode);
            }
        }

      从中可以看出,除了String类型、数组类型和枚举类型,其他对象如果没有实现Serializable接口,都会抛出NotSerializableException异常。

      接下来进行反序列化测试:

        @Test
        public void testDeserialize() throws FileNotFoundException, IOException, ClassNotFoundException{
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("E:\Student.txt")));
            Student s = (Student)ois.readObject();
            System.out.println(s);
            ois.close();
        }

      在进行正常的序列化测试后接着测试反序列化测试,正常来说是不会报错的。看看测试结果就明了了。

      同样上面是正常的情况。接下来异常的情况就能体现出Student类中serialVersionUID变量的作用了:它就是验证序列化与反序列化的唯一性。在序列化后修改Student类中的serialVersionUID= 61234L,然后再测试反序列化,测试结果如下:

     

       从错误提示中可以看出,会从解析出来的serialVersionUID和要转换的class中serialVersionUID做对比,判断是否相等。java.io.ObjectStreamClass关键源码如下:

        /**
         * Initializes class descriptor representing a non-proxy class.
         */
        void initNonProxy(ObjectStreamClass model,
                          Class<?> cl,
                          ClassNotFoundException resolveEx,
                          ObjectStreamClass superDesc)
            throws InvalidClassException
        {
            long suid = Long.valueOf(model.getSerialVersionUID());
            ObjectStreamClass osc = null;
            if (cl != null) {
                osc = lookup(cl, true);
                if (osc.isProxy) {
                    throw new InvalidClassException(
                            "cannot bind non-proxy descriptor to a proxy class");
                }
                if (model.isEnum != osc.isEnum) {
                    throw new InvalidClassException(model.isEnum ?
                            "cannot bind enum descriptor to a non-enum class" :
                            "cannot bind non-enum descriptor to an enum class");
                }
                //这里会判断serialVersionUID是否一致
                if (model.serializable == osc.serializable &&
                        !cl.isArray() &&
                        suid != osc.getSerialVersionUID()) {
                    throw new InvalidClassException(osc.name,
                            "local class incompatible: " +
                                    "stream classdesc serialVersionUID = " + suid +
                                    ", local class serialVersionUID = " +
                                    osc.getSerialVersionUID());
                }
                //后面代码略
            }
        }

     四、几个序列化注意事项

      1、对继承父类的子类序列化

    public class Person {
        public int age;
        
        public int weight;
        
        public Person(int age, int weight) {
            this.age = age;
            this.weight = weight;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public int getWeight() {
            return weight;
        }
    
        public void setWeight(int weight) {
            this.weight = weight;
        }
        
    }
    import java.io.Serializable;
    
    public class Women extends Person implements Serializable{
        
        private static final long serialVersionUID = -1259423203325949704L;
        
        private String name;
      
        public Women(String name, int age, int weight) {
            super(age, weight);
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return "Student [name=" + name + ", age=" + age + ", weight=" + weight + "]";
        }
    }

      单元测试代码如下,先猜错下会不会运行通过,结果又是啥?

        @Test
        public void test2() throws FileNotFoundException, IOException, ClassNotFoundException{
            Women w = new Women("Scarlett Johansson", 34, 50);
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("E:\Women.txt")));
            oos.writeObject(w);
            oos.close();
            
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("E:\Women.txt")));
            Women s = (Women)ois.readObject();
            System.out.println(s);
            ois.close();
        }

      测试结果如下,居然报错。该错误提示需要提供无参构造函数(During deserialization, the fields of non-serializable classes will be initialized using the public or protected no-arg constructor of the class. A no-arg constructor must be accessible to the subclass that is serializable. The fields of serializable subclasses will be restored from the stream.),具体详情解释见这here.

      

      当我们提供无参构造函数后再进行单元测试得到结果:Student [name=Scarlett Johansson, age=0, weight=0]。所以可以得出结论:父类中的字段不参与序列化,只是将其初始化而已。

      2、transient 关键字

       使用transient关键字来修饰变量,然后进行序列化(在实现serializable接口情况下)效果其实和父类的序列化一样,它所修饰的变量不参与序列化。这里就不举例说明了,自己可以写个案例测试下。

      另外transient关键字只能修饰变量, 不能修饰类和方法。

      3、static关键字

      使用static关键字来修饰变量,不管有没有transient修饰,同样不参与序列化(在实现serializable接口情况下)

      4、Externalizable接口

       Externalizable接口extends Serializable接口,而且在其基础上增加了两个方法:writeExternal()和readExternal()。这两个方法会在序列化和反序列化还原的过程中被自动调用,以便执行一些特殊的操作。

      下面看案例:

    import java.io.Externalizable;
    import java.io.IOException;
    import java.io.ObjectInput;
    import java.io.ObjectOutput;
    
    /**
     * @Description: 学生类 已实现序列化接口
     * @author yuanfy
     * @date 2018年1月11日 上午11:36:37 
     * @version 1.0
     */
    public class Student implements Externalizable{
    
        private static final long serialVersionUID = 61234L;
    
        private String name;
        
        private transient int age;
        
        private static String sex;
        
        //如果覆盖了无参构造函数就一定要显示声明无参构造函数,跟父类序列化的案例一样。
        public Student(){}
        
        public Student(String name, int age, String sex) {
            this.name = name;
            this.age = age;
            this.sex = sex;
        }
        
        public  String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
        public String getSex() {
            return sex;
        }
        public void setSex(String sex) {
            this.sex = sex;
        }
        @Override
        public String toString() {
            return "Student [name=" + name + ", age=" + age + ", sex=" + sex + "]";
        }
    
        @Override
        public void writeExternal(ObjectOutput out) throws IOException {
            // TODO Auto-generated method stub
            out.writeUTF(name);
            out.writeInt(age);
            out.writeObject(sex);
        }
    
        @Override
        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            this.name = in.readUTF();
            this.age = in.readInt();
            this.sex = (String)in.readObject();
        }
    }
        @Test
        public void test3() throws FileNotFoundException, IOException, ClassNotFoundException{
            Student s1 = new Student("james", 31, "man");
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("E:\Student.txt")));
            oos.writeObject(s1);
            oos.close();
            
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("E:\Student.txt")));
            Student s2 = (Student)ois.readObject();
            System.out.println(s2);
            ois.close();
        }

      上面是正常完整案例,其中要注意的是,要序列化的类要提供无参构造函数,否则反序列化会报错(When an Externalizable object is reconstructed, an instance is created using the public no-arg constructor, then the readExternal method called. Serializable objects are restored by reading them from an ObjectInputStream.)详解here得出结论如下:

      与Serizable对象不同,使用Externalizabled,就意味着没有任何东西可以自动序列化, 为了正常的运行,我们需要在writeExtenal()方法中将自对象的重要信息写入,从而手动的完成序列化。对于一个Externalizabled对象,对象的默认构造函数都会被调用(包括哪些在定义时已经初始化的字段),然后调用readExternal(),在此方法中必须手动的恢复数据。这就说明在Externalizable接口下不管是transient或static修饰的变量,如果没有指定写入,就不会序列化。

  • 相关阅读:
    操作正则表达式的公共类
    验证码类
    强制转化常用函数
    实现Base64编码与其它编码转换的类
    关于任务管理器踩过的坑,程序员必看!
    每次找Internet选项感到抓狂?一键打开!
    教你避过安装TensorFlow的两个坑
    HTML中id与name的通俗区别
    VS code代码对齐快捷键
    form和table的区别
  • 原文地址:https://www.cnblogs.com/yuanfy008/p/8269337.html
Copyright © 2020-2023  润新知