美文网首页
Java序列化机制

Java序列化机制

作者: 0x70e8 | 来源:发表于2018-04-20 16:42 被阅读0次

    Java序列化机制


    序列化和反序列化

    Java序列化是Java内建的数据(对象)持久化机制,通过序列化可以将运行时的对象数据按照特殊的格式存储到文件中,以便在将来重建对象。

    Java序列化是Java IO的一部分,需要ObjectStream提供支持。支撑序列化机制的基础是运行期对对象的信息(如类信息)和数据(如成员域)的读取和存储,从而将数据的字节流写入文件或字节数组中。反序列化过程则是读取字节流中对象的描述信息和数据,通过指定类的构造器和持久化的数据动态地重建出对象。

    当然,对于一般对象而已,重建后的对象和被序列化到文件中的对象,不会是同一个了(地址),因为可能都不在同一台计算机中,序列化中的“内存地址”这个对象相等的标志,变成了对象的序列号,这也是序列化的名称由来。不过对于枚举对象,因为在反序列化过程做了特殊的处理,保证了对象的唯一性。

    需要注意的是,序列化是保存对象的状态,和类状态无关,所以它不会读取静态数据。


    序列化和反序列化的序列号

    序列号是关联到对象的,是内部流处理的机制,外部是不可见的。

    1. 序列化
    • 序列化的每个对象对应一个序列号,这个序列号是唯一对应一个对象的,相当于对象内存地址的作用;
    • 对于每个首次遇到的对象,会将其数据存储到输出流中;
    • 对于相同的对象(这个相同的判断是通过地址),会引用之前的对象的序列号,相当于引用一个地址;
    1. 反序列化(这个过程序列号就是对象的唯一标识符了)
    • 对于首次遇到的序列号,会根据流中的信息构建一个Java对象;
    • 对于与之前的序列号相同的序列号,直接引用之前构建的对象(这里是对象的内存地址)

    序列化的版本号

    序列化的版本号是在类中定义的private static final long serialVersionUID,这个字段用来标示类的版本,它是数据域类型和方法签名信息通过SHA算法取到的指纹,采用了SHA码的前8个字节,主要用在反序列化过程中的类的校验。如果在反序列化过程中,当前类的serialVersionUID和序列化文件中的serialVersionUID不一致,那么就无法从流中构建对象,并抛出异常。
    那么什么时候这个serialVersionUID会变呢?
    主要有下面两种情况

    • 在实现Serializable接口时,没有定义serialVersionUID属性,但在序列化之后,修改了类结构;
    • 手动修改了serialVersionUID的值;

    serialVersionUID是用来应对类在序列化之后类发生了变更的情况。对于第一个情况,在定义可序列化类时没有定义serialVersionUID,不会影响序列化过程,因为在序列化过程中会自动生成一个写入到流中,如果在之后没有修改类的任何域或方法,反序列化是没有问题的,(因为生成指纹的基础没变,自动生成的指纹即serialVersionUID是一致的)。但是如果在序列化之后修改了类,反序列化就会失败,除非手动加上serialVersionUID值和前面自动生成的值一致。查看旧值的方法是:使用jdk的命令serialver <类名>,能拿到他早期版本的版本号。

    对于第二种情况,一般而言,除非是有此类需求,不然不会手动去破坏反序列化。


    相关的类、接口

    序列化标识接口

    • java.io.Externalizable
    • java.io.Serializable

    序列化机制实现类

    • java.io.ObjectInputStream
    • java.io.ObjectOutputStream

    实现Serializable接口或者Externalizable接口的才可以被序列化。Serializable接口没有任何方法,是一个标记接口,Externalizable接口是Serializable接口的子接口,拥有两个公共方法,用来自定义序列化过程。

    Serializable和Externalizable的区别

    实现了Externalizable接口的类,需要实现接口的两个抽象方法,readExternal和writeExternal,需要读写哪些数据都需要显式的调用流的读写方法,也就是序列化的任务从Java内建转移到了开发者,提供了更大自由度的同时,也提高了复杂度。而且在Externalizable反序列化时,会调用类的public无参构造器,如果类中没有定义无参构造器(没有重载构造器的话,会有默认的无参构造器的),会抛出异常。后面再详细说明此接口的使用。

    对象流是序列化的核心,readObject和writeObject方法从文件中反序列化对象和将对象序列化到文件中。


    使用默认的序列化

    以下是实现了Serializable接口的类,main方法演示了对象的序列化和反序列化的使用。

    public class SerializableUser implements Serializable {
    
        /**
         * 
         */
        private static final long serialVersionUID = 1L;
        private String name;
        private int age;
    
        private SerializableUser(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public SerializableUser() {
        }
    
        @Override
        public String toString() {
            return "User  -> name:" + name + " ,age=" + age;
    
        }
    
        public static void main(String[] args) throws IOException {
            SerializableUser user = new SerializableUser("harry", 19);
            SerialUtil.writeToFile("ser", user);
            SerializableUser u = (SerializableUser) SerialUtil.readFromFile("ser");
            System.out.println(u);
            // Files.deleteIfExists(Paths.get("ser"));
        }
    }
    
    // 工具类
    
    public class SerialUtil {
        public static void writeToFile(String path, Object obj) {
            ObjectOutputStream oos;
            try {
                oos = new ObjectOutputStream(new FileOutputStream(path));
                oos.writeObject(obj);
                oos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        public static Object readFromFile(String path) {
            ObjectInputStream ois = null;
            try {
                ois = new ObjectInputStream(new FileInputStream(path));
                return ois.readObject();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (ois != null)
                    try {
                        ois.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
            }
            return null;
    
        }
    

    结果:

    User  -> name:harry ,age=19
    序列化文件内容:
    aced 0005 7372 0026 6a64 6b2e 7465 7374
    2e73 6572 6961 6c69 7a61 626c 652e 5365
    7269 616c 697a 6162 6c65 5573 6572 0000
    0000 0000 0001 0200 0249 0003 6167 654c
    0004 6e61 6d65 7400 124c 6a61 7661 2f6c
    616e 672f 5374 7269 6e67 3b78 7000 0000
    1374 0005 6861 7272 79
    

    自定义序列化

    想要修改Serializable接口实现类的序列化逻辑,比如有些字段不需要写入,或者自己想控制写入的过程,如想给写入文件的字节流加密等等,就不能直接使用对象流提供的read和write方法了。

    transient关键字

    使用transient关键字修饰的字段,在序列化时会被忽略,不会写入流中。transient意思是瞬时的,既然不是长久的,就代表不要持久化。

    重写writeObject()和readObject()

    在实现类中重写writeObject和readObject方法可以改变序列化的方法调用,会调用类中重写的方法,而不会再走对象流的方法,实现逻辑是在对象流的writeObject和readObject中通过反射判断类中是否重写了方法,然后反射调用。
    一般重写的逻辑是先调用流的defaultWriteObject和defaultReadObject方法,然后追加自己的写入逻辑。这两个default方法是原本对象流的writeObject和readObject方法会调用的,所以具备默认的读写作用。可以参考ArrayList类的设计,ArrayList内部使用数组来存储元素,且数组是会动态扩容的,如果直接序列化数组,会序列化很多的空元素即null到流中,既浪费空间也降低了效率,所以在类中将数组变量标注成transient,然后在重写writeObject和readObect方法,将实际的元素写入对象流和从对象流中读出。

    readResolve()

    这个方法是应对enum出现之前设计的枚举的代替代码和单例代码的措施。
    看一段示例:

    public class SimulateEnum {
    
        /**
         * 模拟一个枚举类,在Java内建枚举出现之前的情况
         */
        static class MyEnum implements Serializable {
            private static final long serialVersionUID = 1L;
            private String name;
            private static int order = 0;
            private int ordernum;
    
            private MyEnum(String name) {
                this.name = name;
                this.ordernum = order++;
            }
    
            public String getName() {
                return name;
            }
    
            public int getOrdernum() {
                return ordernum;
            }
    
            public static final MyEnum A = new MyEnum("A");
            public static final MyEnum B = new MyEnum("B");
            public static final MyEnum C = new MyEnum("C");
        }
    
        public static void main(String[] args) throws IOException {
            MyEnum a = MyEnum.C;
            System.out.println(a.getName());
            System.out.println(a.getOrdernum());
            SerialUtil.writeToFile("enum", a);
            MyEnum rebuilda = (MyEnum) SerialUtil.readFromFile("enum");
            System.out.println(rebuilda.getName());
            System.out.println(rebuilda.getOrdernum());
            System.out.println(a == rebuilda);
            Files.deleteIfExists(Paths.get("enum"));
        }
    }
    

    在示例中我们模拟了一个枚举的实现,我们定义了私有的构造器和几个作为枚举对象的静态对象,我们期待的是枚举是安全的,不会再生成除了定义的三个变量之外的其他变量,但是反序列化过程会破坏这种设计。看结果:

    C
    2
    C
    2
    false

    因为对象的反序列化会创建新的对象的,即使类的构造器是私有的,这会破坏单例模式的设计。为了去保护在Java枚举出现之前的模拟枚举,对象留提供了一个解决措施——在类中实现readResolve方法。在readResolve方法中拦截新对象的生成,使之返回已有的对象。
    我们在MyEnum类中加入以下方法:

    private Object readResolve() throws Exception {
                switch (name) {
                case "A":
                    return A;
                case "B":
                    return B;
                case "C":
                    return C;
                default:
                    throw new Exception();
                }
    
            }
    
    

    再执行返回的结果就是true了。
    那么为什么在反序列真正的枚举的时候就不用再考虑在类中实现readReslove方法呢,原因是ObjectStream对枚举做了支持。看一下调用栈。

    readObejct()-> readObject0()-> checkResolve(readEnum(unshared))->readEnum():

            String name = readString(false);
            Enum<?> result = null;
            Class<?> cl = desc.forClass();
            if (cl != null) {
                try {
                    @SuppressWarnings("unchecked")
                    Enum<?> en = Enum.valueOf((Class)cl, name);
                    result = en;
                } catch (IllegalArgumentException ex) {
                    throw (IOException) new InvalidObjectException(
                        "enum constant " + name + " does not exist in " +
                        cl).initCause(ex);
                }
                if (!unshared) {
                    handles.setObject(enumHandle, result);
                }
            }
    

    看一下源码,实际上和我们自己写的readResolve里面的意思是差不多的。

    Externalizable接口

    一个实现Externalizable接口的类表明它是可被外部化的,也是可序列化,只是这个序列化的责任转移给了外部控制。这个接口的定义:

    public interface Externalizable extends java.io.Serializable {
        
        void writeExternal(ObjectOutput out) throws IOException;
    
        void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
    }
    

    两个抽象方法给开发者自行实现序列化的处理。
    示例:

    public class ExternalizableUser implements Externalizable {
    
        private String name;
        private int age;
    
        private ExternalizableUser(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public ExternalizableUser() {
        }
    
        @Override
        public void writeExternal(ObjectOutput out) throws IOException {
            out.writeObject(name);
            out.writeInt(age);
        }
    
        @Override
        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            name = (String) in.readObject();
            age = in.readInt();
        }
    
        @Override
        public String toString() {
            return "User  -> name:" + name + " ,age=" + age;
    
        }
    
        public static void main(String[] args) throws IOException {
            ExternalizableUser user = new ExternalizableUser("harry",19);
            SerialUtil.writeToFile("ext", user);
            ExternalizableUser u = (ExternalizableUser) SerialUtil.readFromFile("ext");
            System.out.println(u);
            Files.deleteIfExists(Paths.get("ext"));
        }
    }
    //User  -> name:harry ,age=19
    

    如果上面示例中的writeExternal和readExternal方法没有实现,那么返回的对象是一个空的对象,数据是默认值。
    序列化的文件内容是:

    aced 0005 7372 0028 6a64 6b2e 7465 7374
    2e73 6572 6961 6c69 7a61 626c 652e 4578
    7465 726e 616c 697a 6162 6c65 5573 6572
    c8f9 50ef 76db 2f15 0c00 0078 7074 0005
    6861 7272 7977 0400 0000 1378 
    

    感兴趣的同学可以去研究一下序列化文件的格式。

    The end

    相关文章

      网友评论

          本文标题:Java序列化机制

          本文链接:https://www.haomeiwen.com/subject/omkzkftx.html