美文网首页
Java中序列化与transient的使用

Java中序列化与transient的使用

作者: 文景大大 | 来源:发表于2019-05-11 23:46 被阅读0次

    一、序列化

    1.1 什么是序列化,为什么要序列化?

    我们在运行Java程序的时候,各个对象是有状态的。比如,我们创建了如下的一个Fruit对象,它的名字和重量是它当前的状态信息:

    public static void main(String[] args) {
            Fruit apple = new Fruit();
            apple.setName("apple");
            apple.setWeight(23);
        }
    

    在有的场景下,我们需要将该对象当前的状态信息持久化地保留下来,或者借助网络传输到别的地方,而这些场景是无法以对象的形态保留信息的,毕竟只有Java运行的时候才有对象这个概念。

    因此,我们只能以文本或者字符的形式来表示当前Java中该对象的状态信息。那么,将Java中一个动态的对象表示成文本或者字符的过程,我们就称之为序列化

    1.2 如何在Java中使用序列化

    常见的Java序列化方法有Java原生序列化、Hessian序列化、kryo序列化、Json序列化等,这里以介绍Java原生序列化为例。

    默认声明的类是不能支持序列化的,只有当这个类实现了Serializable接口,才可以被序列化。而这个Serializable接口中不包含任何方法和属性,它仅仅是起到一个序列化标识的作用。下面是一个例子:

    @Data
    public class Fruit implements Serializable {
        private String name;
        private Integer weight;
    
        // setters and getters...
    }
    
        public static void main(String[] args) {
            Fruit apple = new Fruit();
            apple.setName("apple");
            apple.setWeight(23);
            saveToFile(apple);
            System.out.println("序列化结束");
        }
    
        private static void saveToFile(Fruit fruit){
            try {
                FileOutputStream fs = new FileOutputStream("fruit.txt");
                ObjectOutputStream os = new ObjectOutputStream(fs);
                os.writeObject(fruit);
                os.flush();
                os.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    

    如果以上Fruit类没有实现Serializable接口,程序运行就会报错:

     java.io.NotSerializableException: cn.zx.demo.Fruit
        at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
        at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
        at cn.zx.demo.Main.saveToFile(Main.java:27)
        at cn.zx.demo.Main.main(Main.java:19)
    

    只有实现了该接口,才能正常运行并生成一个txt文件,这个就是我们apple对象序列化后的结果。

    1.3 反序列化的使用

    反序列化,顾名思义,就是要把以文本形式持久化了的对象信息再还原成Java运行时动态的对象的状态。

    我们使用上面序列化中的例子,将持久化了的文本txt读入:

        public static void main(String[] args) {
            Fruit apple = readFromFile("fruit.txt");
            // print apple
            System.out.println(apple.getName());
            // print 23
            System.out.println(apple.getWeight());
        }
    
        private static Fruit readFromFile(String fileName){
            Fruit fruit = null;
            try{
                FileInputStream fs = new FileInputStream(fileName);
                ObjectInputStream os = new ObjectInputStream(fs);
                fruit = (Fruit) os.readObject();
                os.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            return fruit;
        }
    

    反序列化成功,我们成功地从序列化了的文本信息中恢复了动态的Java对象及其序列化时的状态信息。

    1.4 版本标识serialVersionUID

    当一个对象被序列化后,接着需要被反序列化时,如何判断能否顺利地反序列化形成对象呢?因为原来的类可能被修改了,可能和序列化时的类是不一样的。

    因此,需要使用serialVersionUID用来标识反序列化时类的版本和序列化时类的版本是否一致。当值是一致的,则表示可以序列化,反之则不能序列化,会报如下错误:

    java.io.InvalidClassException: cn.zx.demo.code.Fruit; local class incompatible: stream classdesc serialVersionUID = -4079670274710263332, local class serialVersionUID = -1938591684852446153
        at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
        at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1843)
    

    在默认情况下,serialVersionUID的值不需要我们手动指定,系统会自动指定,只要我们确定序列化和反序列化前后的类没有发生变化,那么就不会出现如上的问题。

    倘若我们将序列化之后的类增加了一个属性,或者加了一个别的逻辑,改变了类的程序结构,那么系统就会自动更改serialVersionUID的值,从而使得反序列化失败,报出如上的错误。比如我们加一个属性:

    @Data
    public class Fruit implements Serializable {
        private String name;
        private Integer weight;
        private Double price;
    
        // setters and getters...
    }
    

    但这是不合理的,因为我们只是加了一个属性,并不妨碍反序列化,新加的属性让其值为空不就行了,所以,让系统自动指定serialVersionUID的值在这种场景下就不是那么合理。我们需要自己指定它的值。

    @Data
    public class Fruit implements Serializable {
        private static final long serialVersionUID = -1L;
        private String name;
        private Integer weight;
    
        // setters and getters...
    }
    

    然后,随便你怎么改动类的结构,只要保证serialVersionUID不变,程序就不会比较这个值是否一致,从而尝试反序列化。

    比如新加的一个属性反序列化后,内存中的它只不过是没有值而已,其它可以反序列化的属性不受影响。如果你改动的是原先序列化时就存在的属性,那么也没有关系,反序列化找不到对应的属性就会跳过,把能反序列化的都赋值,不能的全部为空。

    1.5 序列化的使用注意事项

    • 父类如果实现了序列化的接口,那么其子类自动拥有了序列化的能力;
    • 一个已经实现了序列化的类引用了其它的类,那么其它的类也必须实现序列化的接口,否则序列化过程会报错java.io.NotSerializableException;
    • 静态变量和transient修饰的变量不会被序列化;

    1.6 序列化的常见使用场景

    • 远程方法调用(RPC);
    • 对象存储到文件或者数据库中;
    • 实现对象的深拷贝;

    二、transient

    2.1 transient的作用是什么

    在将对象序列化的时候,并不是所有的字段都需要保存起来。比如一些敏感信息,我们只要求在内存中使用就好,序列化的时候不要连同它们一起持久化。这时候,Java中的transient关键字就派上用场了,被它修饰了的字段就不会被序列化:

    private transient Integer weight;
    

    我们仍然使用上面的序列化和反序列化的例子,只不过给Fruit类中的weight属性加上transient关键字,希望它不要被序列化。

    然后运行一次序列化和反序列的程序,得到的打印结果中weight为null。由此证明了transient生效了。

    2.2 transient的使用注意事项

    • 被transient修饰的变量无法序列化,随后反序列时便无法恢复该变量的值;
    • transient只能用来修饰变量,无法修饰类和方法;
    • 类中的静态变量也是无法序列化的,因为类变量属于类,不属于对象。

    关于如上第三点,给出实验进行证明:

    public class Fruit implements Serializable {
        private static final long serialVersionUID = 6950076107144781481L;
        private String name;
        private transient Integer weight;
        private static String color;
    
        public Fruit(){}
    
        // getters and setters...
    }
    
        public static void main(String[] args) {
            Fruit apple = new Fruit();
            apple.setName("apple");
            apple.setWeight(23);
            Fruit.setColor("red");
            saveToFile(apple);
            System.out.println("序列化结束");
        }
    
        private static void saveToFile(Fruit fruit){
            try {
                FileOutputStream fs = new FileOutputStream("fruit.txt");
                ObjectOutputStream os = new ObjectOutputStream(fs);
                os.writeObject(fruit);
                os.flush();
                os.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    

    先执行如上程序,将设置好静态变量color的apple对象进行序列化,然后再执行如下程序得出反序列化后的结果:

        public static void main(String[] args) {
            Fruit apple = readFromFile("fruit.txt");
            // print apple
            System.out.println(apple.getName());
            // print null
            System.out.println(apple.getWeight());
            // print null
            System.out.println(Fruit.getColor());
        }
    
        private static Fruit readFromFile(String fileName){
            Fruit fruit = null;
            try{
                FileInputStream fs = new FileInputStream(fileName);
                ObjectInputStream os = new ObjectInputStream(fs);
                fruit = (Fruit) os.readObject();
                os.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            return fruit;
        }
    

    三、待解决的疑问

    1. 你最常用的序列化和反序列化的业务场景是什么?
      答复:在1.6中已经列出场景。

    2. 日常开发中,最常使用的场景就是数据库数据的读写、关联方系统Json数据的传输,但是并没有留意到DTO和PO有实现Serializable接口,更没有serialVersionUID的声明,这是为什么呢?它们不算序列化的操作吗?
      答复:Json是Java中的另一种序列化方法,不是原生的序列化方法,不需要实现Serializable接口。

    相关文章

      网友评论

          本文标题:Java中序列化与transient的使用

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