关于序列化:几种Java常用序列化框架的选型与对比

简介: 序列化与反序列化是咱们日常数据长久化和网络传输中常常应用的技术,然而目前各种序列化框架让人目迷五色,不分明什么场景到底采纳哪种序列化框架。本文会将业界开源的序列化框架进行比照测试,别离从通用性、易用性、可扩展性、性能和数据类型与Java语法反对五方面给出比照测试。 作者 | 云烨起源 | 阿里技术公众号 一 背景介绍序列化与反序列化是咱们日常数据长久化和网络传输中常常应用的技术,然而目前各种序列化框架让人目迷五色,不分明什么场景到底采纳哪种序列化框架。本文会将业界开源的序列化框架进行比照测试,别离从通用性、易用性、可扩展性、性能和数据类型与Java语法反对五方面给出比照测试。 通用性:通用性是指序列化框架是否反对跨语言、跨平台。易用性:易用性是指序列化框架是否便于应用、调试,会影响开发效率。可扩展性:随着业务的倒退,传输实体可能会发生变化,然而旧实体有可能还会被应用。这时候就须要思考所抉择的序列化框架是否具备良好的扩展性。性能:序列化性能次要包含工夫开销和空间开销。序列化的数据通常用于长久化或网络传输,所以其大小是一个重要的指标。而编解码工夫同样是影响序列化协定抉择的重要指标,因为现在的零碎都在谋求高性能。Java数据类型和语法反对:不同序列化框架所可能反对的数据类型以及语法结构是不同的。这里咱们要对Java的数据类型和语法个性进行测试,来看看不同序列化框架对Java数据类型和语法结构的反对度。上面别离对JDK Serializable、FST、Kryo、Protobuf、Thrift、Hession和Avro进行比照测试。二 序列化框架1 JDK SerializableJDK Serializable是Java自带的序列化框架,咱们只须要实现java.io.Serializable或java.io.Externalizable接口,就能够应用Java自带的序列化机制。实现序列化接口只是示意该类可能被序列化/反序列化,咱们还须要借助I/O操作的ObjectInputStream和ObjectOutputStream对对象进行序列化和反序列化。 上面是应用JDK 序列化框架进行编解码的Demo: 通用性 因为是Java内置序列化框架,所以自身是不反对跨语言序列化与反序列化。 易用性 作为Java内置序列化框架,无序援用任何内部依赖即可实现序列化工作。然而JDK Serializable在应用上相比开源框架难用许多,能够看到下面的编解码应用十分僵硬,须要借助ByteArrayOutputStream和ByteArrayInputStream才能够残缺字节的转换。 可扩展性 JDK Serializable中通过serialVersionUID管制序列化类的版本,如果序列化与反序列化版本不统一,则会抛出java.io.InvalidClassException异样信息,提醒序列化与反序列化SUID不统一。 java.io.InvalidClassException: com.yjz.serialization.java.UserInfo; local class incompatible: stream classdesc serialVersionUID = -5548195544707231683, local class serialVersionUID = -5194320341014913710下面这种状况,是因为咱们没有定义serialVersionUID,而是由JDK主动hash生成的,所以序列化与反序列化前后后果不统一。 然而咱们能够通过自定义serialVersionUID形式来躲避掉这种状况(序列化前后都是应用定义的serialVersionUID),这样JDK Serializable就能够反对字段扩大了。 private static final long serialVersionUID = 1L;性能 JDK Serializable是Java自带的序列化框架,然而在性能上其实一点不像亲生的。上面测试用例是咱们贯通全文的一个测试实体。 public class MessageInfo implements Serializable { private String username; private String password; private int age; private HashMap<String,Object> params; ... public static MessageInfo buildMessage() { MessageInfo messageInfo = new MessageInfo(); messageInfo.setUsername("abcdefg"); messageInfo.setPassword("123456789"); messageInfo.setAge(27); Map<String,Object> map = new HashMap<>(); for(int i = 0; i< 20; i++) { map.put(String.valueOf(i),"a"); } return messageInfo; }}应用JDK序列化后字节大小为:432。光看这组数字兴许不会感觉到什么,之后咱们会拿这个数据和其它序列化框架进行比照。 ...

April 30, 2021 · 3 min · jiezi

关于序列化:序列化ProtoBuf-与-JSON-的比较

介绍 ProtoBuf 是google团队开发的用于高效存储和读取结构化数据的工具。什么是结构化数据呢,正如字面上表白的,就是带有肯定构造的数据。比方电话簿上有很多记录数据,每条记录蕴含姓名、ID、邮件、电话等,这种构造反复呈现。 同类 XML、JSON 也能够用来存储此类结构化数据,然而应用ProtoBuf示意的数据能更加高效,并且将数据压缩得更小。 原理 ProtoBuf 是通过ProtoBuf编译器将与编程语言无关的特有的 .proto 后缀的数据结构文件编译成各个编程语言(Java,C/C++,Python)专用的类文件,而后通过Google提供的各个编程语言的反对库lib即可调用API。(对于proto构造体怎么编写,可自行查阅文档) ProtoBuf编译器装置 Mac : brew install protobuf 举个例子 1.先创立一个proto文件message.proto syntax = "proto3";message Person { int32 id = 1; string name = 2; repeated Phone phone = 4; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message Phone { string number = 1; PhoneType type = 2; }} 2.创立一个Java我的项目并且将proto文件搁置 src/main/proto 文件夹下 3.编译proto文件至Java版本用命令行 cd 到 src/main 目录下 ...

December 7, 2020 · 2 min · jiezi

关于序列化:serialVersionUID作用是什么以及如何生成的

失常不设置serialVersionUID 的序列化和反序列化先定义一个实体Student.class,须要实现Serializable接口,然而不须要实现get(),set()办法 import java.io.Serializable;public class Student implements Serializable { private int age; private String name; public Student(int age, String name) { this.age = age; this.name = name; } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + '}'; }}测试类,思路是先把Student对象序列化到Student.txt文件,而后再讲Student.txt文件反序列化成对象,输入。 public class SerialTest { public static void main(String[] args) { serial(); deserial(); } // 序列化 private static void serial(){ Student student = new Student(9, "Mike"); try { FileOutputStream fileOutputStream = new FileOutputStream("Student.txt"); ObjectOutputStream objectOutputStream= new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(student); objectOutputStream.flush(); } catch (Exception exception) { exception.printStackTrace(); } } // 反序列化 private static void deserial() { try { FileInputStream fis = new FileInputStream("Student.txt"); ObjectInputStream ois = new ObjectInputStream(fis); Student student = (Student) ois.readObject(); ois.close(); System.out.println(student.toString()); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } }}输入后果,序列化文件咱们能够看到Student.txt,反序列化进去,外面的字段都是不变的,阐明反序列化胜利了。 ...

November 28, 2020 · 4 min · jiezi

关于序列化:适合时间序列数据的计算脚本

工夫序列数据的计算脚本须具备较强的有序计算能力,本文从此类工具中精心筛选了三种,从开发效率、语法表达能力、结构化函数库等方面进行深度比照,考查了各脚本在序号计算、绝对地位计算、有序汇合计算等重点运算上的体现,esProc 在这几款工具中的体现最为杰出。点击适宜工夫序列数据的计算脚本理解详情。 工夫序列数据在这里指按工夫排序的日常业务数据。对工夫序列数据进行计算时,不仅波及季度、月份、工作日、周末等惯例计算,还常常遇到较为简单的有序运算,这就要求脚本语言应具备相应的计算能力。个别用于解决工夫序列数据的计算脚本有SQL、Python Pandas、esProc,上面就让咱们深刻理解这些脚本,看看它们的能力差异。 SQLSQL历史悠久用户泛滥,在其模型框架内早已倒退到极限,简直每种简略运算都能找到对应的SQL解法,这其中就包含有序运算。 比方比上期的例子:表stock1001存储某支股票的交易信息,次要字段有交易日期transDate、收盘价price,请计算每个交易日与上个交易日相比收盘价的增长率。 这个例子属于绝对地位计算,如果应用窗口函数,SQL写法绝对容易: 但有些SQL不反对窗口函数,实现比上期就会麻烦得多: 上述代码之所以麻烦,首先是因为SQL是基于无序汇合的,自身没有序号,不不便进行有序运算,为了实现有序运算,就要为无序汇合硬造出序号列,这个过程须要自关联和分组汇总,代码比较复杂。其次,比上期属于绝对地位计算,如果SQL有绝对序号,这种计算会简略许多,但SQL没有绝对序号,只能将上一行关联到本行,变相实现相邻地位计算,代码因而变得复杂。 基于无序汇合的SQL不不便实现有序运算,窗口函数尽管能够缓解这一情况,但如果运算再简单时,也仍然麻烦。 比方中位数的例子:scores表存储学生问题,次要字段有学生编号studentdid、数学问题math,请计算数学问题的中位数。中位数的定义是:如果记录总数L为偶数,则返回两头两个值的均值(序号别离为L/2和L/2+1);如果L为奇数,则返回惟一的两头值(序号为(L+1)/2)。 SQL计算中位数的代码: 能够看到,尽管曾经应用了窗口函数,但SQL依然很简单。生成序号的过程对于有序汇合来说是多余的,但对SQL来说就是必不可少的步骤,尤其是本例这种必须显式应用序号的状况,这让代码显得简单。SQL实现分支判断也较麻烦,所以对L为奇数的状况进行解决时,并没有返回惟一的两头值,而是两个同样的两头值求均匀,这个技巧尽管能够简化分支判断,但了解起来稍有艰难。 如果应用取整函数,则能够奇妙地跳过判断过程,在简化代码的同时计算出中位数。但这种技巧与中位数的原始定义不同,会造成了解艰难,这里没有采纳。 再看一个稍简单的例子:间断上涨天数。库表AAPL存储某支股票的股价信息,次要字段有交易日期transDate、收盘价price,请计算该股票最长的间断上涨天数。SQL如下: 按天然思路实现这个工作时,应答日期有序的股票记录进行循环,如果本条记录与上一条记录相比是上涨的,则将间断上涨天数(初始为0)加1,如果是上涨的,则将间断上涨天数和以后最大间断上涨天数(初始为0)相比,选出新的以后最大间断上涨天数,再将间断上涨天数清0。如此循环直到完结,以后最大间断上涨天数即最终的最大间断上涨天数。 但SQL不善于有序计算,无奈用上述天然思路实现,只能用一些难懂的技巧。把按日期有序的股票记录分成若干组,间断上涨的记录分成同一组,也就是说,某天的股价比上一天是上涨的,则和上一天记录分到同一组,如果上涨了,则开始一个新组。最初看所有分组中最大的成员数量,也就是最多间断上涨的天数。 对于这两个稍简单的有序运算例子,SQL实现起来就曾经很艰难了,一旦遇到更简单的运算,SQL简直无奈实现。之所以呈现这种后果,是因为SQL的实践根底就是无序汇合,这种人造缺点无论怎样打补丁,都无奈从根本上解决问题。 Python PandasPandas是Python的结构化计算库,常被用作工夫序列数据的计算脚本。 作为结构化计算函数库,Pandas能够轻松实现简略的有序计算。比方,同样计算比上期,Pandas代码是这样的: 上述前两句是为了从文件读取数据,外围代码仅有一句。须要留神的是,Pandas并不能示意前一行,从而间接实现绝对地位计算,但能够用shift(1)函数将列整体下移一行,从而变相实现绝对地位计算。代码中行和列、前一行和下一行看上去很像,初学者容易混同。 作为古代程序语言,Pandas在有序计算方面要比SQL先进,次要体现在Pandas基于有序汇合构建,dataFrame数据类型天生具备序号,适宜进行有序计算。后面那些稍简单的有序计算,用SQL会十分艰难,用Pandas就绝对容易。 同样计算中位数,Pandas外围代码如下: 上述代码中,Pandas能够间接用\[N\]示意序号,而不必额定制作序号,代码因而失去简化。其次,Pandas是过程性语言,分支判断比SQL易于了解,也不须要技巧来简化代码。 同样稍简单的例子最长间断上涨天数,Pandas也比SQL容易实现。外围代码如下: 本例中,Pandas能够依照天然思路实现,而不用采取难懂的技巧,代码的表白效率要比SQL高得多。 有点遗憾的是, 有序计算经常要波及绝对地位计算,但Pandas不能间接表白绝对地位,只能把列下移一行来变相示意本行的上一行,了解时有点艰难。 Pandas在有序计算方面确实比SQL容易些,但遇到更简单的状况,Pandas也会变得很繁琐,下面试举两例。 比方过滤累计值的例子:表sales存储客户的销售额数据,次要字段有客户client、销售额amount,请找出销售额累计占到一半的前n个大客户,并按销售额从大到小排序。Pandas代码如下: 再比方计算股价最高3天的涨幅:表stock1001存储某支股票的每日股价,次要字段有交易日期transDate、收盘价price,请将股价最高的三天按逆序排列,计算每一天相比前一天的涨幅。Pandas代码如下: 这些更简单的例子也须要用到一些难懂的技巧去实现,不仅难以编写,而且难以读懂,这里就不再具体解释。 esProc与Pandas相似,esProc也具备丰盛的结构化计算函数,与Pandas不同的是,esProc除了基于有序汇合并反对序号机制外,还提供了不便的相邻援用机制,以及丰盛的地位函数,从而快捷不便地实现有序计算。 对于简略的有序计算,esProc和其余计算脚本一样,都能够轻松实现。比方同样比上期的esProc代码: 下面代码A1从csv文件取数,A2是外围代码。esProc能够用直观易懂的\[-1\]示意绝对本行的前一行,这是Pandas和SQL都没有的性能,也是esProc更为业余的体现。 同样计算中位数,esProc外围代码如下: 上述代码中,esProc能够间接用\[N\]示意序号,而不必额定制作序号,代码更为简洁。esProc同样是过程性语法,既能够用if/else语句实现大段的分支,也能够像本例一样,用if函数实现简洁的判断。 同样稍简单的例子最长间断上涨天数,esProc也比SQL/Pandas容易实现。外围代码如下: 本例中,esProc能够依照天然思路实现,而不用采取非凡的技巧,代码表白效率要比SQL更高。除此外, esProc既能够用循环语句实现大段的循环,也能够像本例一样,用循环函数max实现简洁的循环聚合。 esProc是更为业余的结构化计算语言,即便遇到更简单的有序计算,也能较为轻松地实现。 比方过滤累计值的例子,esProc只需如下代码: 本例按天然思维实现,先在A2计算出从最大的客户到每个客户的累计值,再在A3算出最大累计值的一半,在A4算出累计值大于A3的地位,最初按地位取数据就是所需后果。这里有体现esProc专业性的两处特色,其一是A3中的m函数,该函数能够逆序取数,-1示意倒数第一条;其二是A4中的pselect,能够按条件返回序号。这两种函数都能够无效简化有序计算。 再比方计算股价最高那3天的涨幅,esProc只需如下代码: 上述代码中,A2中的ptop示意前N条的地位,和后面的pselect相似,返回的不是记录的汇合,而是序号的汇合,相似这样的函数在esProc中还有很多,其目标都是简化有序计算。A4中的#也是esProc的特色,间接示意序号字段,应用起来十分不便,不用像SQL那样额定制作,或Pandas那样设定index。 通过比拟咱们能够发现,esProc具备丰盛的结构化函数,是业余的结构化计算语言,能够轻松实现常见的有序计算,即便更简单的计算也能无效简化,是更加现实的工夫序列数据计算脚本。

November 5, 2020 · 1 min · jiezi

关于序列化:序列化和反序列化

一、序列化的含意、意义及应用场景序列化:将对象写入到IO流中反序列化:从IO流中复原对象意义:序列化机制容许将实现序列化的Java对象转换位字节序列,这些字节序列能够保留在磁盘上,或通过网络传输,以达到当前复原成原来的对象。序列化机制使得对象能够脱离程序的运行而独立存在。应用场景:所有可在网络上传输的对象都必须是可序列化的,比方RMI(remote method invoke,即近程办法调用),传入的参数或返回的对象都是可序列化的,否则会出错;所有须要保留到磁盘的java对象都必须是可序列化的。通常倡议:程序创立的每个JavaBean类都实现Serializeable接口。二、序列化实现的形式果须要将某个对象保留到磁盘上或者通过网络传输,那么这个类应该实现Serializable接口或者Externalizable接口之一。 1、Serializable1.1 一般序列化Serializable接口是一个标记接口,不必实现任何办法。一旦实现了此接口,该类的对象就是可序列化的。 序列化步骤:步骤一:创立一个ObjectOutputStream输入流;步骤二:调用ObjectOutputStream对象的writeObject输入可序列化对象。 public class Person implements Serializable {  private String name;  private int age;  //我不提供无参结构器  public Person(String name, int age) {      this.name = name;      this.age = age;  }  @Override  public String toString() {      return "Person{" +              "name='" + name + ''' +              ", age=" + age +              '}';  }}public class WriteObject {  public static void main(String[] args) {      try (//创立一个ObjectOutputStream输入流           ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) {          //将对象序列化到文件s          Person person = new Person("9龙", 23);          oos.writeObject(person);      } catch (Exception e) {          e.printStackTrace();      }  }}复制代码反序列化步骤:步骤一:创立一个ObjectInputStream输出流;步骤二:调用ObjectInputStream对象的readObject()失去序列化的对象。 咱们将下面序列化到person.txt的person对象反序列化回来 public class Person implements Serializable {  private String name;  private int age;  //我不提供无参结构器  public Person(String name, int age) {      System.out.println("反序列化,你调用我了吗?");      this.name = name;      this.age = age;  }  @Override  public String toString() {      return "Person{" +              "name='" + name + ''' +              ", age=" + age +              '}';  }}public class ReadObject {  public static void main(String[] args) {      try (//创立一个ObjectInputStream输出流           ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.txt"))) {          Person brady = (Person) ois.readObject();          System.out.println(brady);      } catch (Exception e) {          e.printStackTrace();      }  }}//输入后果//Person{name='9龙', age=23}复制代码waht???? 输入通知咱们,反序列化并不会调用构造方法。反序列的对象是由JVM本人生成的对象,不通过构造方法生成。 1.2 成员是援用的序列化如果一个可序列化的类的成员不是根本类型,也不是String类型,那这个援用类型也必须是可序列化的;否则,会导致此类不能序列化。 看例子,咱们新增一个Teacher类。将Person去掉实现Serializable接口代码。 public class Person{    //省略相干属性与办法}public class Teacher implements Serializable {    private String name;    private Person person;    public Teacher(String name, Person person) {        this.name = name;        this.person = person;    }     public static void main(String[] args) throws Exception {        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {            Person person = new Person("路飞", 20);            Teacher teacher = new Teacher("雷利", person);            oos.writeObject(teacher);        }    }}复制代码 咱们看到程序间接报错,因为Person类的对象是不可序列化的,这导致了Teacher的对象不可序列化 1.3 同一对象序列化屡次的机制同一对象序列化屡次,会将这个对象序列化屡次吗?答案是否定的。 public class WriteTeacher {    public static void main(String[] args) throws Exception {        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {            Person person = new Person("路飞", 20);            Teacher t1 = new Teacher("雷利", person);            Teacher t2 = new Teacher("红发香克斯", person);            //顺次将4个对象写入输出流            oos.writeObject(t1);            oos.writeObject(t2);            oos.writeObject(person);            oos.writeObject(t2);        }    }}复制代码顺次将t1、t2、person、t2对象序列化到文件teacher.txt文件中。 留神:反序列化的程序与序列化时的程序统一。 public class ReadTeacher {    public static void main(String[] args) {        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("teacher.txt"))) {            Teacher t1 = (Teacher) ois.readObject();            Teacher t2 = (Teacher) ois.readObject();            Person p = (Person) ois.readObject();            Teacher t3 = (Teacher) ois.readObject();            System.out.println(t1 == t2);            System.out.println(t1.getPerson() == p);            System.out.println(t2.getPerson() == p);            System.out.println(t2 == t3);            System.out.println(t1.getPerson() == t2.getPerson());        } catch (Exception e) {            e.printStackTrace();        }    }}//输入后果//false//true//true//true//true复制代码从输入后果能够看出,Java序列化同一对象,并不会将此对象序列化屡次失去多个对象。 Java序列化算法所有保留到磁盘的对象都有一个序列化编码号当程序试图序列化一个对象时,会先查看此对象是否曾经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输入。如果此对象曾经序列化过,则间接输入编号即可。图示上述序列化过程。 1.4 java序列化算法潜在的问题因为java序利化算法不会反复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保留序列化编号。 public class WriteObject {    public static void main(String[] args) {        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));             ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) {            //第一次序列化person            Person person = new Person("9龙", 23);            oos.writeObject(person);            System.out.println(person);            //批改name            person.setName("海贼王");            System.out.println(person);            //第二次序列化person            oos.writeObject(person);            //顺次反序列化出p1、p2            Person p1 = (Person) ios.readObject();            Person p2 = (Person) ios.readObject();            System.out.println(p1 == p2);            System.out.println(p1.getName().equals(p2.getName()));        } catch (Exception e) {            e.printStackTrace();        }    }}//输入后果//Person{name='9龙', age=23}//Person{name='海贼王', age=23}//true//true复制代码1.5 可选的自定义序列化有些时候,咱们有这样的需要,某些属性不须要序列化。应用transient关键字抉择不须要序列化的字段。 public class Person implements Serializable {   //不须要序列化名字与年龄   private transient String name;   private transient int age;   private int height;   private transient boolean singlehood;   public Person(String name, int age) {       this.name = name;       this.age = age;   }   //省略get,set办法}public class TransientTest {   public static void main(String[] args) throws Exception {       try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));            ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) {           Person person = new Person("9龙", 23);           person.setHeight(185);           System.out.println(person);           oos.writeObject(person);           Person p1 = (Person)ios.readObject();           System.out.println(p1);       }   }}//输入后果//Person{name='9龙', age=23', singlehood=true', height=185cm}//Person{name='null', age=0', singlehood=false', height=185cm}复制代码从输入咱们看到,应用transient润饰的属性,java序列化时,会疏忽掉此字段,所以反序列化出的对象,被transient润饰的属性是默认值。对于援用类型,值是null;根本类型,值是0;boolean类型,值是false。 应用transient尽管简略,但将此属性齐全隔离在了序列化之外。java提供了可选的自定义序列化。能够进行管制序列化的形式,或者对序列化数据进行编码加密等。 private void writeObject(java.io.ObjectOutputStream out) throws IOException;private void readObject(java.io.ObjectIutputStream in) throws IOException,ClassNotFoundException;private void readObjectNoData() throws ObjectStreamException;复制代码通过重写writeObject与readObject办法,能够本人抉择哪些属性须要序列化, 哪些属性不须要。如果writeObject应用某种规定序列化,则相应的readObject须要相同的规定反序列化,以便能正确反序列化出对象。这里展现对名字进行反转加密。 public class Person implements Serializable {   private String name;   private int age;   //省略构造方法,get及set办法   private void writeObject(ObjectOutputStream out) throws IOException {       //将名字反转写入二进制流       out.writeObject(new StringBuffer(this.name).reverse());       out.writeInt(age);   }   private void readObject(ObjectInputStream ins) throws IOException,ClassNotFoundException{       //将读出的字符串反转复原回来       this.name = ((StringBuffer)ins.readObject()).reverse().toString();       this.age = ins.readInt();   }}复制代码当序列化流不残缺时,readObjectNoData()办法能够用来正确地初始化反序列化的对象。例如,应用不同类接管反序列化对象,或者序列化流被篡改时,零碎都会调用readObjectNoData()办法来初始化反序列化的对象。 更彻底的自定义序列化 ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException; ...

September 15, 2020 · 1 min · jiezi