你确定你懂序列化?

Java基础之序列化篇

Hello,大家好!我是Laochou,一位又老又丑的前行者!今天主要是分享一下序列化的知识。

那么,

序列化是什么?

序列化是将对象(内存中)的状态信息转换为可以存储或传输的形式的过程。
大白话就是,将对象以某种形式存储在某种介质上。

既然对象被某种形式存储下来了,那么如何还原呢?

有一个策略叫反序列化。反序列化,顾名思义,就是将某种形式的存储转换为原来的对象。

序列化的用途

  • 需要将内存中的对象保存到某种介质上。
  • 需要将内存中的对象进行网络传输。
  • RPC 调用

Java中如何支持序列化

在Java是通过两种方式来支持序列化的。下面会一一道来。

Serializable

这个接口,它孑然一身。没有继承其他接口(接口之间是可以继承的)。而且它里面没有任何待实现的方法。

public interface Serializable {
}

因此,这个Serializable接口只起到标识作用,只有实现了这个接口才可以被序列化,否则会抛出异常。

序列化到底存储那些信息(看定义是将对象的状态信息)。所以序列化是无法序列化静态成员变量的,注意坑点。

我们先定义一个Person类(大家都比较熟悉,哈哈哈)

public class Person implements Serializable {
    // 姓名
    private String name;
    // 年龄
    private int age;
    // get and set method
    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;
    }
    // 这里一定要重写 toString方法,否则打印对象就是地址了。
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + ''' +
                ", age=" + age +
                '
}';
    }
}

接下来,就是我们通过一个主类来进行序列化了。(代码见名知意,所以注释很少)

import java.io.*;
public class Main {
    // user.dir 这个属性很重要,以后你们的文件上传也有可能使用到
    // 通过这个属性,我们可以拿到当前项目的路径。
    private static final String prefix = System.getProperty("user.dir");
    public static void main(String[] args) {
        Person person = new Person();
        person.setName("laochou");
        person.setAge(19);
        serialPerson(person);
        deserialPerson();
    }
    // 序列化方法
    public static void serialPerson(Person person) {
        ObjectOutputStream objectOutputStream = null;
        FileOutputStream fileOutputStream = null;
        try {
            File file = new File(prefix+ "\" + "person.txt");
            // 判断文件是否存在
            if(!file.exists()) {
                boolean result = file.createNewFile();
                if(result) {
                    System.out.println("创建文件成功");
                }else {
                    System.out.println("创建文件失败,序列化过程提前终止");
                    return;
                }
            }
            fileOutputStream = new FileOutputStream(file);
            objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(person);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            // 记住资源一定是先开后关,后开先开。因为资源有一定的依赖顺序
            try {
                if(objectOutputStream != null) {
                    objectOutputStream.close();
                }
                if(fileOutputStream != null) {
                    fileOutputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    // 反序列方法
    public static void deserialPerson() {
        ObjectInputStream objectInputStream = null;
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream(prefix + "\" + "person.txt");
            objectInputStream = new ObjectInputStream(fileInputStream);
            Person person = (Person) objectInputStream.readObject();
            System.out.println(person);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }finally {
            // 记住资源一定是先开后关,后开先开。因为资源有一定的依赖顺序
            try {
                if(objectInputStream != null) {
                    objectInputStream.close();
                }
                if(fileInputStream != null) {
                    fileInputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

运行之后,效果截图。你确定你懂序列化?可以看到程序在项目的根目录下,创建了一个person.txt的文件。当然如果你打开person.txt,你会发现里面的内容看不懂(我也看不懂)。后面我们通过程序来读出来看。

到了这里,你的第一个序列化和反序列化程序OK了。恭喜!!!,但是真的这么简单吗?

serialVersionUID

现在有个问题,就是Person类,如果我新增一个属性,但是我们使用原有的序列化出来的文件进行反序列化,会怎么样呢。看下去

新增属性的证据照,别说我骗你们。你确定你懂序列化?

主方法其他的都注释掉,只留下反序列化的方法。

public static void main(String[] args) {
//        Person person = new Person();
//        person.setName("laochou");
//        person.setAge(19);
//        serialPerson(person);
        deserialPerson();
}

运行程序之后,你会发现。Oh my god! 出了一个大问题。我们来看下这个问题

java.io.InvalidClassException: cn.laochou.pojo.Person; local class incompatible: stream classdesc serialVersionUID = 1841332452983081360, local class serialVersionUID = 8171920675667862597
 at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
 at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2001)
 at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1848)
 at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2158)
 at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)
 at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)
 at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)
 at Main.deserialPerson(Main.java:60)
 at Main.main(Main.java:15)

遇到问题,千万别慌。既然问题来了,那我们就解决问题呗。兵来将挡,水来土掩。

我们先看下报错的信息,翻译一下就是本地类不兼容。这个兼容不兼容是通过什么来判断的呢。继续看下去。流中的类(也就是我们序列化存储的)的serialVersionUID是1841332452983081360,而我们本地类的是serialVersionUID是8171920675667862597。 正常人都能发现这两个serialVersionUID不同吧,当然你要非说一样,那我就捶你。

懂了,它们是通过 serialVersionUID 来判断是否兼容。

那么如何解决上面的报错呢?????

serialVersionUID这个属性我们类中没有啊,它是怎么来的。我们把这个属性删掉或者设置统一默认值,那不就兼容了??

没错,我们就是通过设置统一默认值来达到兼容的效果。好了,我们就勉为其难的设置下,要不然它报错。(在这里千万注意,我们需要先删掉Person类的no属性,加上serialVersionUID属性之后,重新序列化形成文件之后,再添加no属性,然后只反序列化。)

public class Person implements Serializable {
    // 新增
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    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;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + ''' +
                ", age=" + age +
                '
}';
    }
}

重新序列化与反序列化,看下是否存在问题。

public static void main(String[] args) {
      Person person = new Person();
      person.setName("laochou");
      person.setAge(19);
      serialPerson(person);
      deserialPerson();
}

做好上面的操作,我们就可以新增一个no属性了,并添加get和set方法,且重写toString方法。

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    // 新增属性,我们在之前序列化的过程中是没有no属性的哦。大家得理清楚。
    private int no;
    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 int getNo() {
        return no;
    }
    public void setNo(int no) {
        this.no = no;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + ''' +
                ", age=" + age +
                ", no=" + no +
                '
}';
    }
}

进行反序列化操作。

public static void main(String[] args) {
//        Person person = new Person();
//        person.setName("laochou");
//        person.setAge(19);
//        serialPerson(person);
      deserialPerson();
}

很神奇,报错没有了哟。叫你报错嚣张,现在Laochou分分钟消灭你。效果如下你确定你懂序列化?大家可以看到,新增属性会给一个默认值。基本类型和引用类型不同哈,引用类型是null。如果大家不知道基本类型可以阅读JAVA有哪些基本类型?

那有人又要说了,你这新增属性OK,那你现在删除一个原有属性会怎么样???

这就来

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    // 我们在之前序列化的时候是两个属性,一个name,一个age。现在改为一个
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + ''' +
                '
}';
    }
}

运行之后,报错没有了,只有一个对象打印呢。那岂不是很好。效果如下你确定你懂序列化?

好了,大家现在应该知道serialVersionUID的重要性了。它对于序列化和反序列的健壮性来讲是至关重要的。 如果我们不默认设置serialVersionUID的值,那么程序会根据类名、接口名、成员变量、成员方法等来生成一个。由此,大家可以想到,新增属性和删减属性,生成的条件改变了,生成的值自然也变了,因此报错就不奇怪了。

重要的事情说几篇???serialVersionUID很重要,很重要,很重要。

到底是IDE不讲武德,还是自己修行不到位?

serialVersionUID 如此重要,IDE也不提醒我,搞得后期维护(改代码)头痛的一批。我想说:“年轻人,我劝你耗子尾汁”。好了,你肯定说就这?没多少知识量啊

Laochou只能说:“还有!!!”。

transient

这个关键字也是十分重要的呢。(具体案例我就不写了,自己动手一试)

现在有一个场景,对象里面的一些信息是不能被序列化的,因为保密协议。一旦序列化,反序列化岂不就是泄密了。

这个时候,我们就可以使用transient关键字了,我们可以使用这个关键字来修饰我们的成员变量,那么在序列化的时候,进行忽略。

那么就有人又问了,难道使用了transient就一定无法序列化???

答案当时是:不是!使用了transient,也可能序列化。

Laochou骗我,上面都说了被忽略了,还能序列化。我这就举个JDK源码例子好吧,你不相信我,难道还不相信JDK源码吗?ArrayList的elementData数组成员变量就被transient修饰了,但是ArrayList支持序列化,这里是一个扩展点。因为ArrayList重写了writeObject和readObject方法。如果你能结合这个源码案例来说明transient,我相信面试官肯定眼前一亮。

面试官问你序列化的知识,你讲个Serializable就行???耗子尾汁

Externalizable

Externalizable 也是一个接口。继承了Serializable接口。

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

大家注意到,Externalizable接口有两个待实现的方法。因此使用这种方式来达到序列化效果是必须实现两个方法的哦。

public class Animal implements Externalizable {
    private String name;
    private int age;
    // 最重要的便是重写的这两个方法
    // 指定属性序列化(不指定就啥也不序列化)
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeObject(age);
    }
    // 反序列化
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = (String) in.readObject();
        age = (int) in.readObject();
    }
    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;
    }
    @Override
    public String toString() {
        return "Animal{" +
                "name='" + name + ''' +
                ", age=" + age +
                '
}';
    }
}

注意,就算我们的属性使用了transient修饰,但是在writeExternal方法中指定了序列化,还是会被序列化的。不信大家可以试一试哦。(具体的序列化方法和反序列化方法流程不变,参数需要改变一下)

public static void serialAnimal(Animal animal) {
      ObjectOutputStream objectOutputStream = null;
      FileOutputStream fileOutputStream = null;
      try {
          File file = new File(prefix+ "\" + "animal.txt");
          if(!file.exists()) {
              boolean result = file.createNewFile();
              if(result) {
                  System.out.println("创建文件成功");
              }else {
                  System.out.println("创建文件失败,序列化过程提前终止");
                  return;
              }
          }
          fileOutputStream = new FileOutputStream(file);
          objectOutputStream = new ObjectOutputStream(fileOutputStream);
          objectOutputStream.writeObject(animal);
      } catch (IOException e) {
          e.printStackTrace();
      }finally {
          // 记住资源一定是先开后关,后开先开。因为资源有一定的依赖顺序
          try {
              if(objectOutputStream != null) {
                  objectOutputStream.close();
              }
              if(fileOutputStream != null) {
                  fileOutputStream.close();
              }
          } catch (IOException e) {
              e.printStackTrace();
          }
      }
  }

  public static void deserialAnimal() {
      ObjectInputStream objectInputStream = null;
      FileInputStream fileInputStream = null;
      try {
          fileInputStream = new FileInputStream(prefix + "\" + "animal.txt");
          objectInputStream = new ObjectInputStream(fileInputStream);
          Animal animal = (Animal) objectInputStream.readObject();
          System.out.println(animal);
      } catch (IOException | ClassNotFoundException e) {
          e.printStackTrace();
      }finally {
          // 记住资源一定是先开后关,后开先开。因为资源有一定的依赖顺序
          try {
              if(objectInputStream != null) {
                  objectInputStream.close();
              }
              if(fileInputStream != null) {
                  fileInputStream.close();
              }
          } catch (IOException e) {
              e.printStackTrace();
          }
      }
  }

非常非常重要的细节: Externalizable这个接口来反序列化,是必须使用无参构造参数的,而Serializable也有这个要求。如何验证,大家都知道,如果我们不写构造方法,那么系统会默认帮我们加上一个无参构造方法。但是如果我们指定了一个有参构造方法,系统的默认无参构造方法就失效了。因此

public class Animal implements Externalizable {
    private transient String name;
    private int age;
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeObject(age);
    }
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = (String) in.readObject();
        age = (int) in.readObject();
    }
    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;
    }
    @Override
    public String toString() {
        return "Animal{" +
                "name='" + name + ''' +
                ", age=" + age +
                '
}';
    }
    // 加上这个方法便可验证。
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

效果如下你确定你懂序列化?

有很多人会问,为什么一定要用无参构造方法呢?

因为使用Externalizable进行反序列化,当读取对象时,会调用被序列化类的无参构造方法去创建一个新的对象;然后再将被保存对象的字段的值分别填充到新对象中。这也就是为什么反序列化的过程中,无参构造方法会被调用。由于这个原因,实现Externalizable接口的类必须要提供一个无参的构造方法,且它的访问权限为public。

如何阅读序列化的txt文件呢

在这里推荐一款可以直接看16进制的编辑器 Sublime

用改款编辑器打开之后只有下面的东东,以下都是16进制奥

aced 0005 7372 0016 636e 2e6c 616f 6368
6f75 2e70 6f6a 6f2e 5065 7273 6f6e 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 0007 6c61 6f63 686f 75

aced:流协议的魔数约定为 aced,其实就是一个标记,class文件的魔数是cafebabe
0005:代表流协议版本
7372:73 代表是一个对象,72之后就是我们类的一个描述。
0016:类名的长度,因为是16进制的吗,算一下就知道是22,因此类名的长度是22。也就是11组。
636e 2e6c 616f 6368 6f75 2e70 6f6a 6f2e 5065 7273 6f6e:类的名称。大家肯定不信是吧,我用Python帮你们验证
这是Python验证的命令,你们也可以自己尝试下。

[chr(int(i,16)) for i in "0x63 0x6e 0x2e 0x6c 0x61 0x6f 0x63 0x68 0x6f 0x75 0x2e 0x70 0x6f 0x6a 0x6f 0x2e 0x50 0x65 0x72 0x73 0x6f 0x6e".split()]

你确定你懂序列化?拼起来就是 cn.laochou.pojo.Person

后面的我就不翻译了,大家想探究的话,可以自己百度或者看书查找资料。

细节注定成败,懂的都懂,别辜负我特意翻译一波的辛苦。

请回答

小伙伴们如果看到这里的话,那么非常感谢小伙伴的支持哦。因为能看到这里,实属不易。

大家反正都这么辛苦了,那么就在辛苦下呗,回答几个问题(自问自答也行呀)。帮大家回顾

  • Java中支持序列化的方式有几种,分别是什么?
  • 使用Serializable来序列化的时候,应该注意哪些属性?(UID是不是,都说多少次了)
  • 如果不需要序列化某个属性,应该用什么关键字修饰,用了这个关键字难道就一定不能序列化吗(举出一个案列)
  • 使用Externalizable来支持序列化的时候,需要重写哪两个方法。没有哪一个构造方法会报错??
  • 思考题:你觉得XML和JSON格式来存储是属于序列化吗?

大家可以在我们的QQ交流群进行交流这些问题。交流群(加群不失联)

一些话

这篇推文中,不仅仅是单纯的序列化知识,也注入了很多的思考和一些扩展的点。希望小伙伴能get到一些知识。

微信公众号推文,推荐使用电脑看。因为干货肯定是有代码的,希望小伙伴可以克服下,当然文章我会同步到我掘金账号上。掘金搜索“Laochou”,在用户那一栏就可以找到我。

码字不易,觉得写得还不错的话,欢迎大家点赞、关注、转发支持啦。我们重在分享,但是也需要你们的星星之火燎我们的平原。

你们的支持就是我们创作的最大动力。

我是Laochou,一位又老又丑的前行者!下期见。

往期推荐:

我们是FingerDance,欢迎加入我们!


原文始发于微信公众号(FingerDance):你确定你懂序列化?

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/26796.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!