解读Android中的序列化与Json解析

Serializable

我们在日常工作中,网络数据传输最主流的的格式就是 json,我们常使用 Gson 开源框架来处理它,那么它们是如何工作的呢?本篇文章将解读 Android 中的序列化与 json 解析,如:Java 语言提供的 Serializable、Android 提供的 Parceable


本篇文章的示例代码放在 Github 上,所有知识点,如图:

no-shadow

序列化的定义及相关概念

在系统的底层,数据传输形式是字节序列形式传输,系统并不认识对象,只认识字节序列,那么我们如何进行进程间的通讯,我们需要先将数据序列化,就是将对象传化为字节序列;反序列化,当底层的字节序列传输到相应的进程时,就需要反序列化,就是字节序列转化为对象。

在进程间通讯、本地数据存储、网络数据传输都离不开序列化,对于不同的场景选择合适的序列化方案对应用的性能有着极大的影响。

序列化

数据结构或对象转换成二进制的过程,就是将数据结构或对象转换成可以存储或者传输的数据格式的过程。

反序列化

二进制串转换成数据结构或对象的过程,就是序列化生成的二进制串数据被还原成数据结构或对象的过程。

序列化和反序列化的目的

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

Serializable接口

Serializable 是 Java 提供的序列化接口,它非常简单,如下

public interface Serializable {
}

Serializable 只是一个标记,用来被 ObjectOutputStream 序列化,被 ObjectInputStream 反序列化。

Serializable的使用

我们先来新建一个 Student 类,代码如下:

public class Student implements Serializable {

    private static final long serialVersionUID = 7911650650846382143L;
    private String name;
    private Integer age;
    private List<Course> courses;

    // 获取课程
    public List<Course> getCourses() {
        return courses;
    }
    // 新增课程
    public void addCourse(Course course) {
        this.courses.add(course);
    }

    // 用 transient 关键字标记的成员变量不参与序列化
    private transient Date createTime;
    // 静态成员变量属于类而不属于对象,也不参与序列化
    private static SimpleDateFormat dateFormat = new SimpleDateFormat();

    public Student(String name, Integer age) {
        this.name = name;
        this.age = age;
        courses = new ArrayList<>();
        createTime = new Date();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return “Student{“ +
                “name=’” + name + ‘\’’ +
                “, age=” + age +
                “, courses=” + courses +
                “, createTime=” + createTime +
                ‘}’;
    }
}

Student 类依赖于 Course 类,所以,我们再新建一个 Course 类,如下:

public class Course implements Serializable {

    private static final long serialVersionUID = 7980496416494451794L;
    private String name;
    private float score;

    public Course(String name, float score) {
        this.name = name;
        this.score = score;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public float getScore() {
        return score;
    }

    public void setScore(float score) {
        this.score = score;
    }

    @Override
    public String toString() {
        return “Course{“ +
                “name=’” + name + ‘\’’ +
                “, score=” + score +
                ‘}’;
    }
}

我们再新建一个序列化的工具类,如下:

public class SerializableUtil {

    private static String path = System.getProperty(“user.dir”) + “/serializable/src/main/java/net/lishaoy/serializable/serializable/out/student.out”;

    public static synchronized boolean serializable(Object o) {
        if (o == null) {
            return false;
        }
        ObjectOutputStream outputStream = null;
        try {
            outputStream = new ObjectOutputStream(new FileOutputStream(path));
            outputStream.writeObject(o);
            outputStream.close();
            System.out.println(“序列化成功!”);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return false;
    }

    public static synchronized <T> reverseSerializable() {
        ObjectInputStream inputStream = null;

        try {
            inputStream = new ObjectInputStream(new FileInputStream(path));
            Object object = inputStream.readObject();
            System.out.println(“反序列化成功!\n”  + object);
            return (T) object;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}

我们使用 SerializableUtil 工具类,来序列化和反序列化我们的 Student,如下:

public class UseSerializable {

    public static void main(String[] args) {
        Student student = new Student(“lsy”66);
        student.addCourse(new Course(“英语”,66));
        // 序列化
        SerializableUtil.serializable(student);
        // 反序列化
        SerializableUtil.reverseSerializable();
    }

}

运行结果,如下:

序列化成功!
反序列化成功!
Student{name=‘lsy’, age=66, courses=[Course{name=‘英语’, score=66.0}], createTime=null}

在使用 Serializable 时,可以发现以下几个特点:

  • 需要现象 Serializable 的类,才可以序列化和反序列化
  • transient 关键字标记的成员变量不参与序列化
  • 静态成员变量不参与序列化
  • 一个实现序列化的类,它的子类也是可序列化的

serialVersionUID与兼容性

serialVersionUID的作用: 用来表明类的不同版本间的兼容性。Java 序列化机制会通过判断类的 serialVersionUID 来验证版本一致性;在反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较。

兼容性问题: 为了在反序列化时,确保类版本的兼容性,最好在每个要序列化的类中加入 private static final long serialVersionUID = XXX 属性。如果不显式定义该属性,这个属 性值将由JVM根据类的相关信息计算,而修改后的类的计算 结果与修改前的类的计算结果往往不 同,从而造成对象的反序列化因为类版本不兼容而失败。

serialVersionUID 可以用 Android Studio 自动生成,如图:

no-shadow

使用时按 option + enter,如图:

no-shadow

Externalizable接口

JDK 提供了 Serializable 接口外还提供了 Externalizable 接口,它继承了 Serializable,优先级高于 Serializable,源码如下:

public interface Externalizable extends Serializable {
    void writeExternal(ObjectOutput var1) throws IOException;

    void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException;
}

Externalizable接口的使用

我们来看下简单的使用,代码如下:

public class Course implements Externalizable {

    private static final long serialVersionUID = -342346458732794596L;
    private String name;
    private float score;

    public Course(){}

    public Course(String name, float score) {
        this.name = name;
        this.score = score;
        System.out.println(“Course: ” + “name ” + name + “ score ” + score);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public float getScore() {
        return score;
    }

    public void setScore(float score) {
        this.score = score;
    }

    @Override
    public void writeExternal(ObjectOutput objectOutput) throws IOException {
        System.out.println(“writeExternal …”);
        objectOutput.writeObject(name);
    }

    @Override
    public void readExternal(ObjectInput objectInput) throws IOException, ClassNotFoundException {
        System.out.println(“readExternal …”);
        name = (String) objectInput.readObject();
    }

    @Override
    public String toString() {
        return “Course{“ +
                “name=’” + name + ‘\’’ +
                “, score=” + score +
                ‘}’;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Course course = new Course(“数学”,66);
        // 序列化
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
        outputStream.writeObject(course);
        byte[] bytes = byteArrayOutputStream.toByteArray();
        outputStream.close();

        // 反序列化
        ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
        System.out.println((Course)inputStream.readObject());

    }
}

运行结果如下:

Course: name 数学 score 66.0
writeExternal …
readExternal …
Course{name=‘数学’, score=0.0}

BUILD SUCCESSFUL in 576ms

可以看到,在序列化时会调用 writeExternal 方法,反序列化时会调用 readExternal 方法,也就是说我们可以灵活的控制想序列化的字段。

Externalizable 接口,在反序列化时,需要写默认的空构造函数,否则报错:InvalidClassException

使用Serializable的注意点

readObject和writeObject

readObjectwriteObject 并没有在 Serializable 接口里定义,但是通过查看源码,可知,如:ObjectOutputStream 点击进入源码,如下:




private void writeSerialData(Object var1, ObjectStreamClass var2) throws IOException {
    ClassDataSlot[] var3 = var2.getClassDataLayout();

    for(int var4 = 0; var4 < var3.length; ++var4) {
        ObjectStreamClass var5 = var3[var4].desc;
        // hasWriteObjectMethod 会判断我们是否重写了 writeObject() 方法
        if (var5.hasWriteObjectMethod()) {
            ObjectOutputStream.PutFieldImpl var6 = this.curPut;
            this.curPut = null;
            SerialCallbackContext var7 = this.curContext;
            if (extendedDebugInfo) {
                this.debugInfoStack.push(“custom writeObject data (class \”” + var5.getName() + “\”)”);
            }

            try {
                this.curContext = new SerialCallbackContext(var1, var5);
                this.bout.setBlockDataMode(true);
                // 通过反射执行 writeObject() 方法
                var5.invokeWriteObject(var1, this);
                this.bout.setBlockDataMode(false);
                this.bout.writeByte(120);
            } finally {
                this.curContext.setUsed();
                this.curContext = var7;
                if (extendedDebugInfo) {
                    this.debugInfoStack.pop();
                }

            }

            this.curPut = var6;
        } else {
            this.defaultWriteFields(var1, var5);
        }
    }
}



所以,我们也可以像 Externalizable 接口提供的 writeExternalreadExternal 方法一样使用 readObjectwriteObject 来灵活的序列化和反序列化。例如:

public class ReadWriteObjectCourse implements Serializable {

    private static final long serialVersionUID = -6828110073372979297L;
    private String name;
    private float score;

    public ReadWriteObjectCourse(){}

    public ReadWriteObjectCourse(String name, float score) {
        this.name = name;
        this.score = score;
        System.out.println(“Course: ” + “name ” + name + “ score ” + score);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public float getScore() {
        return score;
    }

    public void setScore(float score) {
        this.score = score;
    }
    // 重写 writeObject() 方法,只序列化 name 字段
    private void writeObject(ObjectOutputStream outputStream) throws IOException {
        System.out.println(“writeObject …”);
        outputStream.writeObject(name);
    }
    // 重写 readObject() 方法,只反序列化 name 字段
    private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
        System.out.println(“readObject …”);
        name = (String) inputStream.readObject();
    }

    @Override
    public String toString() {
        return “Course{“ +
                “name=’” + name + ‘\’’ +
                “, score=” + score +
                ‘}’;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ReadWriteObjectCourse course = new ReadWriteObjectCourse(“数学”,66);
        // 序列化
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
        outputStream.writeObject(course);
        byte[] bytes = byteArrayOutputStream.toByteArray();
        outputStream.close();

        // 反序列化
        ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
        ReadWriteObjectCourse course1 = (ReadWriteObjectCourse) inputStream.readObject();
        System.out.println(course1);
    }
}

运行结果,如下:

Course: name 数学 score 66.0
writeObject …
readObject …
Course{name=‘数学’, score=0.0}

BUILD SUCCESSFUL in 722ms

readObjectwriteObject 方法都被执行,自定义序列化 name 字段。Serializable 接口除了这2个方法可以重写外,还有2个方法,分别是 readResolvewriteReplace

多引用写入

多引用写入 问题,我们来看如下代码:

public static void main(String[] args) throws IOException, ClassNotFoundException {
    Course course = new Course(“数学”,66);
    // 序列化
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
    outputStream.writeObject(course);
    course.setName(“英语”);
    outputStream.writeObject(course);
    byte[] bytes = byteArrayOutputStream.toByteArray();
    outputStream.close();

    // 反序列化
    ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
    Course course1 = (Course) inputStream.readObject();
    Course course2 = (Course) inputStream.readObject();
    System.out.println(course1);
    System.out.println(course2);
}

运行结果如下:

Course: name 数学 score 66.0
writeExternal …
readExternal …
Course{name=‘数学’, score=0.0}
Course{name=‘数学’, score=0.0}

BUILD SUCCESSFUL in 557ms

我们 course.setName(“英语”);name 修改后,重新 outputStream.writeObject(course); 但是结果并没有改变。这个就是多引用问题:对于一个实例的多个引用,为了节省空间,只会写入一次。

我们可以使用 outputStream.reset(); 来解决问题。

子类实现 Serializable,而父类没有实现 Serializable

子类实现 Serializable,而父类没有实现 Serializable 问题,我们新建一个 Person 类,代码如下:

public class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return “Person{“ +
                “name=’” + name + ‘\’’ +
                ‘}’;
    }
}

Student 类继承它,代码如下:

public class Student extends Person implements Serializable {

    …

    public Student(String name, Integer age) {
        super(name);
        this.name = name;
        this.age = age;
        courses = new ArrayList<>();
        createTime = new Date();
        System.out.println(“Student: name:” + name + “ age:” + age + “ createTime:” + createTime);
    }

    …

}

运行结果如下:

java.io.InvalidClassException: net.lishaoy.serializable.serializable.Student; no valid constructor

提示我们没有构造函数,我们需要在父类 Person 加入无参的构造函数,如下:

public class Person {
    private String name;
    // 加入无参构造函数
    public Person(){}

    public Person(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return “Person{“ +
                “name=’” + name + ‘\’’ +
                ‘}’;
    }
}

单例模式的序列化

序列化会导致单例失效,就是序列化前后会产生多个对象,代码如下:

public class Single {

    public static class SingleClass implements Serializable {

        private static final long serialVersionUID = 9153534024695280942L;
        private static boolean flag = false;

        private static SingleClass singleClass;

        public static SingleClass getInstance() {

            if(singleClass == null) {
                synchronized (SingleClass.class) {
                    if (singleClass == null) {
                        singleClass = new SingleClass();
                    }
                }
            }

            return singleClass;
        }

        private SingleClass() {
            if (!flag) flag = trueelse throw new RuntimeException(“单例模式被侵犯!”);
        }
    }

    public static void main(String[] args) {
        SingleClass singleClass = SingleClass.getInstance();
        // 序列化
        SerializableUtil.serializable(singleClass);
        // 反序列化
        SingleClass singleClass1 = SerializableUtil.reverseSerializable();
        System.out.println(“序列化之前:” + singleClass.hashCode());
        System.out.println(“序列化之后:”+ singleClass1.hashCode());
    }

}

运行结果,如下:

序列化成功!
反序列化成功!
net.lishaoy.serializable.serializable.Single$SingleClass@41629346
序列化之前:1442407170
序列化之后:1096979270

BUILD SUCCESSFUL in 621ms

单例模式序列化前后会产生多个对象的问题,可以重写 readResolve() 方法解决。

Parcelable接口

Parcelable 是 Android SDK 为我们提供的序列化接口,它是基于内存的,由于内存读写速度高于硬盘,因此 Android 中的跨进程对象的传输一般使用 ParcelableParcelable 相对于 Serializable 的使用复杂一些,但是 Parcelable 的效率比 Serializable 也高很多

Parcelable的使用

由于 Parcelable 是 Android SDK 提供的,所以,需要在 Android 工程下使用,如下:

public class Course implements Parcelable {

    private static final String TAG = “Course”;
    private String name;
    private float score;

    @Override
    public String toString() {
        return “Course{“ +
                “name=’” + name + ‘\’’ +
                “, score=” + score +
                ‘}’;
    }

    public Course(Parcel in) {
        this.name = in.readString();
        this.score = in.readFloat();
    }
    // 反序列化,将 Parcel 对象转换为 Parcelable
    public static final Creator<Course> CREATOR = new Creator<Course>() {
        //反序列化的方法,将Parcel还原成Java对象
        @Override
        public Course createFromParcel(Parcel in) {
            return new Course(in);
        }
        //提供给外部类反序列化这个数组使用。
        @Override
        public Course[] newArray(int size) {
            return new Course[size];
        }
    };

    public Course(String name, float score) {
        this.name = name;
        this.score = score;
    }

    @Override
    public int describeContents() {
        return 0;
    }
    // 序列化,将对象转换成一个 Parcel 对象
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(this.name);
        dest.writeFloat(this.score);
    }
}

MainActivity 里通过 Intent 来传递数据,如:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Course.runParcel();

        Button button = findViewById(R.id.button);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, ParcelActivity.class);
                intent.putExtra(“course”new Course(“数学”66f));
                startActivity(intent);
            }
        });
    }
}

ParcelActivity 接受数据并打印,如下:

public class ParcelActivity extends AppCompatActivity {

    private static final String TAG = “ParcelActivity”;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_parcel);

        Intent intent = getIntent();
        Parcelable course = intent.getParcelableExtra(“course”);
        Log.i(TAG, “onCreate: ” + course.toString());
    }
}

运行结果,如下:

I/ParcelActivity: onCreate: Course{name=‘数学’, score=66.0}

Parcelable与Serializable的性能比较

Serializable性能分析

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

Parcelable性能分析

Parcelable 则是以 IBinder 作为信息载体,在内存上开销比较小,因此在内存之间进行数据传递时,推荐使用 Parcelable,而 Parcelable 对数据进行持久化或者网络传输时操作复杂,一般这个时候推荐使用 Serializable

JSON解析方式

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,通常用于:数据标记,存储,传输。

Android Studio自带org.json解析

org.json 解析是基于文档驱动,需要把全部文件读入到内存中,然后遍历所有数据,根据需要检索想要 的数据,具体使用,如下:

public class OrgJsonActivity extends AppCompatActivity {

    private static final String TAG = “OrgJsonActivity”;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_org_json);
        try {
            createJson();
            parseJson();
        } catch (JSONException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void createJson() throws JSONException, IOException {
        File file = new File(getFilesDir(), “orgJson.json”);
        JSONObject student = new JSONObject();
        student.put(“name”,“lsy”);
        student.put(“age”66);
        JSONObject course = new JSONObject();
        course.put(“name”,“数学”);
        course.put(“score”,66);
        student.put(“course”, course);
        JSONArray courses = new JSONArray();
        courses.put(0, course);
        student.put(“courses”,courses);
        FileOutputStream outputStream = new FileOutputStream(file);
        outputStream.write(student.toString().getBytes());
        outputStream.close();
        Log.i(TAG, “createJson: ” + student.toString());
    }

    private void parseJson() throws IOException, JSONException {
        File file = new File(getFilesDir(), “orgJson.json”);
        FileInputStream inputStream = new FileInputStream(file);
        InputStreamReader streamReader = new InputStreamReader(inputStream);
        BufferedReader reader = new BufferedReader(streamReader);
        String line;
        StringBuffer stringBuffer = new StringBuffer();
        while ((line = reader.readLine()) != null) {
            stringBuffer.append(line);
        }
        inputStream.close();
        streamReader.close();
        reader.close();

        Student student = new Student();
        JSONObject jsonObject = new JSONObject(stringBuffer.toString());
        String name = jsonObject.optString(“name”“lsy”);
        int age = jsonObject.optInt(“age”66);
        student.setName(name);
        student.setAge(age);

        JSONArray courses = jsonObject.optJSONArray(“courses”);
        for (int i = 0; i < courses.length(); i++) {
            JSONObject course = courses.getJSONObject(i);
            Course course1 = new Course();
            course1.setName(course.optString(“name”,“”));
            course1.setScore((float) course.optDouble(“score”0));
            student.addCourse(course1);
        }

        Log.i(TAG, “parseJson: ” + student);

    }
}

运行结果,如下:

I/OrgJsonActivity: createJson: {“name”:“lsy”,“age”:66,“course”:{“name”:“数学”,“score”:66},“courses”:[{“name”:“数学”,“score”:66}]}
I/OrgJsonActivity: parseJson: Student{id=0, name=‘lsy’, age=66, courses=[Course{name=‘数学’, score=66.0}]}

Gson解析

Gson 解析也是基于事件驱动,它根据所需取的数据 建立1个对应于JSON数据的JavaBean类,即可通过简单操作解析出 所需数据,具体使用如下:

public class GsonActivity extends AppCompatActivity {

    private static final String TAG = “GsonActivity”;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_gson);
        createGson();
    }

    public void createGson() {
        Student student = new Student(1,“lsy”66);
        student.addCourse(new Course(“英语”,66));
        // 序列化
        Gson gson = new Gson();
        String json = gson.toJson(student);
        Log.i(TAG, “createGson: json ” + json);
        // 反序列化
        Log.i(TAG, “createGson: json1” + gson.fromJson(json, Student.class));
    }
}

运行结果如下:

I/GsonActivity: createGson: json {“age”:66,“courses”:[{“name”:“英语”,“score”:66.0}],“id”:1,“name”:“lsy”}
I/GsonActivity: createGson: json1Student{id=1, name=‘lsy’, age=66, courses=[Course{name=‘英语’, score=66.0}]}

Json 解析方式还有 Jackson 解析、Fastjson解析等,在此就不具体介绍。

Gson原理解析

在序列化和反序列化的过程中,Gson 充当了一个解析器的角色,如图

no-shadow

JsonElement

该类是一个抽象类,代表着 json 串的某一个元素。这个元素可以是一个 Json(JsonObject)、可以是一个数组(JsonArray)、可以是一个Java的基本类型( JsonPrimitive)、当然也可以为 null( JsonNull);JsonObject、JsonArray、JsonPrimitive、JsonNull 都是 JsonElement 这个抽象类的子类。JsonElement 提供了一系列的方法来判断当前的JsonElement。

JsonObject 对象可以看成 name/values 的集合,而这写 values 就是一个个 JsonElement,他们的结构可以 用如下图表示:

no-shadow

Gson的工作流程

Gson的工作流程,如图

no-shadow
本文结束感谢您的阅读

本文标题:解读Android中的序列化与Json解析

文章作者:李少颖(persilee)

发布时间:2020年08月04日 - 02:08

最后更新:2020年08月04日 - 03:08

原始链接:https://h.lishaoy.net/serializable.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!