美文网首页
Java基础-序列化

Java基础-序列化

作者: 涛涛123759 | 来源:发表于2021-04-22 07:30 被阅读0次

    Android知识总结

    一、定义

    1)、序列化
    将数据结构或对象转换成二进制串 (stream 流) 的过程。

    2)、反序列化
    将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。

    3)、序列化协议需要考虑

    • 1、序列化后的流大小(占用网络宽带)
    • 2、序列化/反序列化的性能(CPU资源占用)
    • 3、是否支持跨语言

    4)、序列化/反序列化的目的

    • 序列化: 主要用于网络传输,数据持久化,一般序列化也称为编码(Encode)
    • 反序列化: 主要用于从网络,磁盘上读取字节数组还原成原始对象,一般反序列化也称为解码
      (Decode)

    具体的讲:

    • 永久的保存对象数据(将对象数据保存在文件当中,或者是磁盘中)
    • 通过序列化操作将对象数据在网络上进行传输(由于网络传输是以字节流的方式对数据进行传输的,因此序列化的目的是将对象数据转换成字节流的形式)
    • 将对象数据在进程之间进行传递(Activity之间传递对象数据时,需要在当前的Activity中对对象数据进行序列化操作,在另一个Activity中需要进行反序列化操作讲数据取出)
    • Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长(即每个对象都在JVM中)
    • 但在现实应用中,就可能要停止JVM运行,但有要保存某些指定的对象,并在将来重新读取被保存的对象。这是Java对象序列化就能够实现该功能。(可选择入数据库、或文件的形式保存)
    • 序列化对象的时候只是针对变量进行序列化,不针对方法进行序列化。
    • 在Intent之间,基本的数据类型直接进行相关传递即可,但是一旦数据类型比较复杂的时候,就需要进行序列化操作了。

    二、几种常见的序列化和反序列化协议

    1)、XML&SOAP
    XML 是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点,SOAP(Simple Object Access protocol) 是一种被广泛应用的,基于 XML 为序列化和反序列化协议的结构化消息传递协议

    2)、JSON(Javascript Object Notation)
    JSON 起源于弱类型语言 Javascript, 它的产生来自于一种称之为"Associative array"的概念,其本质是就是采用"Attribute-value"的方式来描述对象。实际上在 Javascript 和 PHP 等弱类型语言中,类的描述方式就是 Associative array。JSON 的如下优点,使得它快速成为最广泛使用的序列化协议之一。

    • 1、这种 Associative array 格式非常符合工程师对对象的理解。
    • 2、它保持了 XML 的人眼可读(Human-readable)的优点。
    • 3、相对于 XML 而言,序列化后的数据更加简洁。 来自于的以下链接的研究表明:XML 所产生序列化之后文件的大小接近 JSON 的两倍
    • 4、它具备 Javascript 的先天性支持,所以被广泛应用于 Web browser 的应用常景中,是 Ajax 的事实标准协议。
    • 5、与 XML 相比,其协议比较简单,解析速度比较快。
    • 6、松散的 Associative array 使得其具有良好的可扩展性和兼容性。

    3)、Protobuf
    Protobuf 具备了优秀的序列化协议的所需的众多典型特征。

    • 1、标准的 IDL 和 IDL 编译器,这使得其对工程师非常友好。
    • 2、序列化数据非常简洁,紧凑,与 XML 相比,其序列化之后的数据量约为 1/3 到 1/10。
    • 3、解析速度非常快,比对应的 XML 快约 20-100 倍。
    • 4、提供了非常友好的动态库,使用非常简介,反序列化只需要一行代码。

    三、Serializable接口

    Serializable是Java中的序列化接口,其使用起来简单但开销较大(因为Serializable在序列化过程中使用了反射机制,故而会产生大量的临时变量,从而导致频繁的GC),并且在读写数据过程中,它是通过IO流的形式将数据写入到硬盘或者传输到网络上

    是 Java 提供的序列化接口,它是一个空接口:

    public interface Serializable {
    }
    

    Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。

    1、示例
    实例化对象

    class PeopleBean : Serializable {
        lateinit var name: String
        var age: Int = 0
        lateinit var sex: String
        //Course也需要实现Serializable接口
        var courses : List<Course> ? = null
        //用transient关键字标记的成员变量不参与序列化(在被反序列化后,
        //transient 变量的值被 设为初始值,如 int 型的是 0,对象型的是 null)
       @Transient
        var createTime: Date = Date()
    
        constructor()
        constructor(name: String, age: Int, sex: String, courses: List<Course>) {
            this.name = name
            this.age = age
            this.sex = sex
            this.courses = courses
        }
    
        companion object{
            //serialVersionUID唯一标识了一个可序列化的类,
            //如果不写在你添加字段后,反序列化时后报错  ,因为此时serialVersionUID发生变化  
            val serialVersionUID = 14555662223
        }
    
        override fun toString(): String {
            return "PeopleBean(name='$name', age=$age, sex=$sex, " +
                    "createTime=$createTime,courses=$courses)"
        }
    
    
    }
    
    //Course也需要实现Serializable接口
    class Course : Serializable{
        var name : String
        var score : Float
    
        constructor(name: String, score: Float) {
            this.name = name
            this.score = score
        }
    
        override fun toString(): String {
            return "Course(name='$name', score=$score)"
        }
        companion object{
            val serialVersionUID = 14555662224
        }
    }
    

    序列化和反序列化调用执行

    object SerializeText {
        var path = "D:\\11\\dd\\dd.out"
        fun <T> writeObject(obj: T) : Boolean{
            if (obj == null){
                return false
            }
            try {
                val file = File(path)
                //返回的是File类型,可以调用exsit()等方法
                val parentFile = file.parentFile
                // 能创建多级目录
                if (!parentFile.exists()){
                    parentFile.mkdirs()
                }
                //有路径才能创建文件
                if (!file.exists()) {
                    file.createNewFile()
                }
                val out = ObjectOutputStream(FileOutputStream(file))
                out?.writeObject(obj)
                out.close()
                return true
            } catch (e: Exception) {
                e.printStackTrace()
                return false
            }
        }
        fun <T> readObject(): T? {
            var put: ObjectInputStream? = null
            try {
                val file = File(path)
                val parentFile = file.parentFile
                // 能创建多级目录
                if (!parentFile.exists()){
                    parentFile.mkdirs()
                }
                //有路径才能创建文件
                if (!file.exists()) {
                    file.createNewFile()
                }
                put = ObjectInputStream(FileInputStream(file))
                return put!!.readObject() as T
            } catch (e: Exception) {
                e.printStackTrace()
            } finally {
                put?.close()
            }
            return null
        }
    }
    
    fun main() {
        var course1 = Course("科一", 98.5f)
        var course2 = Course("科二", 98.5f)
        val list = ArrayList<Course>()
        list.add(course1)
        list.add(course2)
        val people = PeopleBean("XaoMing", 15, "男", list)
        println("序列化之前==> " + people.toString())
        SerializeText.writeObject(people)
        val readObject = SerializeText.readObject<PeopleBean>()
        println("反序列化之后==> " + readObject.toString())
    }
    

    2、Serializable 有以下几个特点:

    • 1、可序列化类中,未实现 Serializable 的属性状态无法被序列化/反序列化
    • 2、也就是说,反序列化一个类的过程中,它的非可序列化的属性将会调用无参构造函数重新创建
    • 3、因此这个属性的无参构造函数必须可以访问,否者运行时会报错
    • 4、一个实现序列化的类,它的子类也是可序列化的

    3、serialVersionUID与兼容性

    • serialVersionUID的作用
      serialVersionUID 用来表明类的不同版本间的兼容性。如果你修改了此类, 要修改此值。否则以前serialVersionUID 用来表明类的不同版本间的兼容性。如果你修改了此类, 要修改此值。否则以前。

    • 设置方式
      在JDK中,可以利用JDK的bin目录下的serialver.exe工具产生这个serialVersionUID,对于 Test.class,执行命令:serialver Test

    • 兼容性问题
        为了在反序列化时,确保类版本的兼容性,最好在每个要序列化的类中加入 private static final long serialVersionUID这个属性,具体数值自己定义。这样,即使某个类在与之对应的对象 已经序列化出去后做了修改,该对象依然可以被正确反序列化。否则,如果不显式定义该属性,这个属性值将由JVM根据类的相关信息计算,而修改后的类的计算 结果与修改前的类的计算结果往往不同,从而造成对象的反序列化因为类版本不兼容而失败。
        不显式定义这个属性值的另一个坏处是,不利于程序在不同的JVM之间的移植。因为不同的编译器实现该属性值的计算策略可能不同,从而造成虽然类没有改变,但是因为JVM不同,出现因类版本不兼容而无法正确反序列化的现象出现

    因此 JVM 规范强烈 建议我们手动声明一个版本号,这个数字可以是随机的,只要固定不变就可以。同时最好是 private 和 final 的,尽量保证不变。

    4、注意

    • 4.1、不被序列化
    //用transient关键字标记的成员变量不参与序列化(在被反序列化后,transient 变量的值被 设为初始值,如 int 型的是 0,对象型的是 null) 
    private transient Date createTime; 
    //静态成员变量属于类不属于对象,所以不会参与序列化(对象序列化保存的是对象的“状态”,也 就是它的成员变量,因此序列化不会关注静态变量) 
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
    

    transient作用的字段不会被序列化。

    • 4.2、如果类是可序列化的, 但其超类没有序列化,怎么是其父类序列化。
    • 父类
    /**
     * 1. 如果不需要保存父类的值,那么没什么问题,只不过序列化会丢失父类的值
     * 2. 如果在子类保存父类的值,则需要在父类提供一个无参构造,不然报错InvalidClassException
     * 子类在序列化的时候需要额外的序列化父类的域(如果有这个需要的话)。那么在反序列的时候,
     * 由于构建User实例的时候需要先调用父类的构造函数,然后才是自己的构造函数。
     * 反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象,因此当我们取父对象的变量值时,
     * 它的值是调用父类无参构造函数后的值。如果你考虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化。
     * 或者在readObject方法中进行赋值。我们只需要在Person中添加一个空的构造函数即可
     * 3. 自定义序列化过程
     */
    class Person{
            private String sex;
            private int id;
            public Person() {
            }
            public Person(String sex, int id) {
                this.sex = sex;
                this.id = id;
            }
            public String getSex() {
                return sex;
            }
            public void setSex(String sex) {
                this.sex = sex;
            }
            public int getId() {
                return id;
            }
            public void setId(int id) {
                this.id = id;
            }
            @Override
            public String toString() {
                return "Person{" +
                        "sex='" + sex + '\'' +
                        ", id=" + id +
                        '}';
            }
        }
    
    • 子类
    class User extends Person implements Serializable {
            public User(String name, int age,String sex,int id) {
                super(sex,id);
                this.name = name;
                this.age = age;
            }
            public User(){
                super();
            };
            public String name;
            public int age;
            private void writeObject(ObjectOutputStream out) throws IOException {//不是重写父类的方案
                out.defaultWriteObject();
                out.writeObject(getSex());
                out.writeInt(getId());
            }
    
            private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
                in.defaultReadObject();
                setSex((String)in.readObject());
                setId(in.readInt());
            }
            @Override
            public String toString() {
                return "User{" +
                        "name='" + name + '\'' +
                        ", age=" + age +
                        "} " + super.toString();
            }
        }
    
    • 4.3、父类可序列化,如何阻止子类可序列化
      实现writeObject,readObject,readObjectNoData 在这些方法里面抛出运行时异常。
    • 父类
    class Person implements Serializable {
            private static final long serialVersionUID = 5850510148907441688L;
            private String sex;
            private int id;
            public Person() {
            }
            public Person(String sex, int id) {
                this.sex = sex;
                this.id = id;
            }
            public String getSex() {
                return sex;
            }
            public void setSex(String sex) {
                this.sex = sex;
            }
            public int getId() {
                return id;
            }
            public void setId(int id) {
                this.id = id;
            }
            @Override
            public String toString() {
                return "Person{" +
                        "sex='" + sex + '\'' +
                        ", id=" + id +
                        '}';
            }
        }
    
    • 子类防止序列化
    class User1 extends Person {
            public User1(String name, int age, String sex, int id) {
                super(sex, id);
                this.name = name;
                this.age = age;
    
            }
            
            public String name;
            public int age;
    
            private void writeObject(ObjectOutputStream out) throws IOException {
                throw new NotSerializableException("Can not serialize this class");
            }
    
            private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
                throw new NotSerializableException("Can not serialize this class");
            }
    
            private void readObjectNoData() throws ObjectStreamException {
                throw new NotSerializableException("Can not serialize this class");
            }
    
            @Override
            public String toString() {
                return "User{" +
                        "name='" + name + '\'' +
                        ", age=" + age +
                        "} " + super.toString();
            }
        }
    
    • Main 函数中调用
     private static void main(String[] args) {
            User1 user = new User1("XaoMing", 18, "男", 1);
            System.out.println("1: " + user);
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ObjectOutputStream oos = null;
            byte[] userData = null;
            try {
                oos = new ObjectOutputStream(out);
                oos.writeObject(user);
                userData = out.toByteArray();
            } catch (IOException e) {
                e.printStackTrace();
            }
            
            ObjectInputStream ois = null;
            try {
                ois = new ObjectInputStream(new ByteArrayInputStream(userData));
                user = (User1) ois.readObject();
                System.out.println("反序列化后 2: " + user);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    四、Externalizable接口

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

    1、使用
    序列化类

    class User implements Externalizable {
            //必须要一个public的无参构造函数
            public User(){}
            public User(String name, int age) {
                this.name = name;
                this.age = age;
            }
            public String name;
            public int age;
            @Override
            public String toString() {
                return "User{" +
                        "name='" + name + '\'' +
                        ", age=" + age +
                        '}';
            }
            //写和读必须顺序保持一致
            @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();
            }
        }
    

    五、Java的序列化步骤与数据结构分析

    序列化算法一般会按步骤做如下事情:

    • 将对象实例相关的类元数据输出。
    • 递归地输出类的超类描述直到不再有超类。
    • 类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
    • 从上至下递归输出实例的数据

    六、Parcelable 接口

    Parcelable是Android为我们提供的序列化的接口,Parcelable相对于Serializable的使用相对复杂一些,但Parcelable的效率相对Serializable也高很多,这一直是Google工程师引以为傲的,有时间的可以看一下Parcelable和Serializable的效率对比 Parcelable vs Serializable 号称快10倍的效率。

    Parcelable是Android SDK提供的,它是基于内存的,由于内存读写速度高于硬盘,因此Android中的跨进程对象的传递一般使用Parcelable。

    public class Course implements Parcelable {
        private String name;
        private float score;
    
        /**
         * 描述当前 Parcelable 实例的对象类型
         *  比如说,如果对象中有文件描述符,这个方法就会返回上面的CONTENTS_FILE_DESCRIPTOR
         *  其他情况会返回一个位掩码
         */
        @Override
        public int describeContents() {
            return 0;
        }
    
        /**
         * 将对象转换成一个 Parcel 对象
         * @param dest 表示要写入的 Parcel 对象
         * @param flags 示这个对象将如何写入       
         */
        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeString(this.name);
            dest.writeFloat(this.score);
        }
    
        protected Course(Parcel in) {
            this.name = in.readString();
            this.score = in.readFloat();
        }
    
        /**
         * 实现类必须有一个 Creator 属性,用于反序列化,将 Parcel 对象转换为 Parcelable  
         */
        public static final Parcelable.Creator<Course> CREATOR = new Parcelable.Creator<Course>() {
            //反序列化的方法,将Parcel还原成Java对象
            @Override
            public Course createFromParcel(Parcel source) {
                return new Course(source);
            }
    
            //提供给外部类反序列化这个数组使用。
            @Override
            public Course[] newArray(int size) {
                return new Course[size];
            }
        };
    }
    

    1、Parcel的简介
    在介绍之前我们需要先了解Parcel是什么?Parcel翻译过来是打包的意思,其实就是包装了我们需要传输的数据,然后在Binder中传输,也就是用于跨进程传输数据。
    简单来说,Parcel提供了一套机制,可以将序列化之后的数据写入到一个共享内存中,其他进程通过Parcel可以从这块共享内存中读出字节流,并反序列化成对象,下图是这个过程的模型。

    Parcel可以包含原始数据类型(用各种对应的方法写入,比如writeInt(),writeFloat()等),可以包含Parcelable对象,它还包含了一个活动的IBinder对象的引用,这个引用导致另一端接收到一个指向这个IBinder的代理IBinder。

    Parcelable通过Parcel实现了read和write的方法,从而实现序列化和反序列化。

    七、Parcelable与Serializable的性能比较

    1、Serializable性能分析
    Serializable是Java中的序列化接口,其使用起来简单但开销较大(因为Serializable在序列化过程中使用了反射机制,故而会产生大量的临时变量,从而导致频繁的GC),并且在读写数据过程中,它是通过IO流的形式将数据写入到硬盘或者传输到网络上。

    2、Parcelable性能分析
    Parcelable则是以IBinder作为信息载体,在内存上开销比较小,因此在内存之间进行数据传递时,推荐使用Parcelable。

    而Parcelable对数据进行持久化或者网络传输时操作复杂,一般这个时候推荐使用Serializable。

    3、性能比较总结描述
    首先Parcelable的性能要强于Serializable的原因我需要简单的阐述一下

    • 在内存的使用中,前者在性能方面要强于后者
    • 后者在序列化操作的时候会产生大量的临时变量,(原因是使用了反射机制)从而导致GC的频繁调用,因此在性能上会稍微逊色
    • Parcelable是以Ibinder作为信息载体的.在内存上的开销比较小,因此在内存之间进行数据传递的时候,Android推荐使用Parcelable,既然是内存方面比价有优势,那么自然就要优先选择。
    • 在读写数据的时候,Parcelable是在内存中直接进行读写,而Serializable是通过使用IO流的形式将数据读写入在硬盘上。

    但是:虽然Parcelable的性能要强于Serializable,但是仍然有特殊的情况需要使用Serializable,而不去使用Parcelable,因为Parcelable无法将数据进行持久化,因此在将数据保存在磁盘的时候,仍然需要使用后者,因为前者无法很好的将数据进行持久化.(原因是在不同的Android版本中,Parcelable可能会不同,因此数据的持久化方面仍然是使用Serializable)

    4、两种如何选择

    • 在使用内存方面,Parcelable比Serializable性能高,所以推荐使用Parcelable。
    • Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的GC。
    • Parcelable不能使用在要将数据存储在磁盘上的情况,因为Parcelable不能很好的保证数据的持续性,在外界有变化的情况下,建议使用Serializable。

    八、几个面试相关的问题

    • 1、Android里面为什么要设计出Bundle而不是直接用Map结构

    Bundle内部是由ArrayMap实现的,ArrayMap的内部实现是两个数组,一个int数组是存储对象数据对应下标,一个对象数组保存key和value,内部使用二分法对key进行排序,所以在添加、删除、查找数据的时候,都会使用二分法查找,只适合于小数据量操作,如果在数据量比较大的情况下,那么它的性能将退化。而HashMap内部则是数组+链表结构,所以在数据量较少的时候,HashMap的Entry Array比ArrayMap占用更多的内存。因为使用Bundle的场景大多数为小数据量,我没见过在两个Activity之间传递10个以上数据的场景,所以相比之下,在这种情况下使用
    ArrayMap保存数据,在操作速度和内存占用上都具有优势,因此使用Bundle来传递数据,可以保证更快的速度和更少的内存占用。

    另外一个原因,则是在Android中如果使用Intent来携带数据的话,需要数据是基本类型或者是可序列化类型,HashMap使用Serializable进行序列化,而Bundle则是使用Parcelable进行序列化。而在Android平台中,更推荐使用Parcelable实现序列化,虽然写法复杂,但是开销更小,所以为了更加快速的进行数据的序列化和反序列化,系统封装了Bundle类,方便我们进行数据的传输。

    • 2、Android中Intent/Bundle的通信原理及大小限制

    Intent 中的 Bundle 是使用 Binder 机制进行数据传送的。能使用的 Binder 的缓冲区是有大小限制的(有些手机是 2 M),而一个进程默认有 16 个 Binder 线程,所以一个线程能占用的缓冲区就更小了( 有人以前做过测试,大约一个线程可以占用 28KB)。所以当你看到 The Bindertransaction failed because it was too large 这类 TransactionTooLargeException 异常时,你应该知道怎么解决了

    • 3、为何Intent不能直接在组件间传递对象而要通过序列化机制?

    Intent在启动其他组件时,会离开当前应用程序进程,进入ActivityManagerService进程(intent.prepareToLeaveProcess()),这也就意味着,Intent所携带的数据要能够在不同进程间传输。首先我们知道,Android是基于Linux系统,不同进程之间的java对象是无法传输,所以我们此处要对对象进行序列化,从而实现对象在 应用程序进程 和ActivityManagerService进程 之间传输。

    而Parcel或者Serializable都可以将对象序列化,其中、Serializable使用方便,但性能不如Parcel容器,后者也是Android系统专门推出的用于进程间通信等的接口。


    • 4、在 Java 中的序列化和反序列化过程中使用哪些方法?

    考察你是否熟悉 readObject() 的用法、writeObject()、readExternal() 和 writeExternal()。Java 序列化由java.io.ObjectOutputStream类完成。该类是一个筛选器流, 它封装在较低级别的字节流中, 以处理序列化机制。要通过序列化机制存储任何对象, 我们调用ObjectOutputStream.writeObject(savethisobject), 并反序列化该对象, 我们称之为ObjectInputStream.readObject()方法。调用以writeObject() 方法在 java 中触发序列化过程。关于 readObject() 方法, 需要注意的一点很重要一点是, 它用于从持久性读取字节, 并从这些字节创建对象, 并返回一个对象, 该对象需要类型强制转换为正确的类型。

    • 5、假设新类的超级类实现可序列化接口, 如何避免新类被序列化?

    对于序列化一个对象需调用ObjectOutputStream.writeObject(saveThisObject), 并用
    ObjectInputStream.readObject() 读取对象, 但 Java 虚拟机为你提供的还有一件事, 是定义这两个方法。如果在类中定义这两种方法, 则 JVM 将调用这两种方法, 而不是应用默认序列化机制。你可以在此处通过执行任何类型的预处理或后处理任务来自定义对象序列化和反序列化的行为。

    • 6、如果类是可序列化的, 但其超类不是, 则反序列化后从超级类继承的实例变量的状态如何?

    Java 序列化过程仅在对象层次都是可序列化结构中继续, 即实现 Java 中的可序列化接口, 并且从超级类继承的实例变量的值将通过调用构造函数初始化, 在反序列化过程中不可序列化的超级类。

    • 7、如果类中的一个成员未实现可序列化接口, 会发生什么情况?

    如果尝试序列化实现可序列化的类的对象,但该对象包含对不可序列化类的引用,则在运行时将引发不可序列化异常 NotSerializableException。

    • 8、序列化时,你希望某些成员不要序列化?你如何实现它?

    有时候也会变着形式问,比如问什么是瞬态 trasient 变量, 瞬态和静态变量会不会得到序列化等,所以,如果你不希望任何字段是对象的状态的一部分, 然后声明它静态或瞬态根据你的需要, 这样就不会是在 Java 序列化过程中被包含在内。

    推荐

    http://gityuan.com/2016/03/12/start-activity/

    相关文章

      网友评论

          本文标题:Java基础-序列化

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