4.Java进阶基础-序列化

序列化与反序列化概念

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

Serializable 接口介绍

  1. Serializable 接口中没有任何方法字段
    它只是一个标识 tags

ObjectOutput
ObjectStreamClass:描述一个对象的结构

需要通过对象的IO流辅助实现系列化

  1. Externalizeable
  • writeExternal(ObjectOutput out)
  • readExternal(ObjectOutput out)

(1)读写顺序要求一致
读写的成员变量的个数一致 否则报错:java.io.EOFException

(2)如果使用 Externalizeable 实现序列化,必须要有一个 public 修饰的无参构造函数 否则报错:java.io.InvalidException
为什么呢?

Serializable 序列化原理

Serializable序列化是指将Java对象转换为字节流的过程,以便能够将对象的状态保存到文件、通过网络传输或者在进程间传递。通过反序列化,这些字节流可以被恢复为原始的Java对象。

Serializable序列化的原理主要包括以下几个关键点:

  1. 标记接口Serializable是一个标记接口(marker interface),这意味着它不包含任何方法。一个类只要实现了这个接口,就表示这个类的实例可以被序列化。
  2. ObjectOutputStream和ObjectInputStream:Java提供了两个重要的类来处理序列化和反序列化:
    • ObjectOutputStream用于将对象序列化为字节流。
    • ObjectInputStream用于将字节流反序列化为对象。
  3. writeObject和readObject:在序列化过程中,对象的状态(字段值)会通过writeObject方法写入字节流,而在反序列化过程中,readObject方法则被用来重建对象状态。
  4. transient关键字:如果某个字段不需要被序列化,可以使用transient修饰符标记它。在序列化时,这些被标记为transient的字段将不会被写入字节流。
  5. 序列化版本UID:为了确保反序列化时的兼容性,建议每个可序列化的类定义一个serialVersionUID
  • Java在反序列化时会检查这个UID,如果不匹配,则会抛出InvalidClassException
  • serialVersionUID可以手动定义,也可以通过序列化工具自动生成。
  1. 实现Serializable的限制:只有非静态和非瞬态的成员变量会被序列化,静态字段不会被序列化。这是因为静态字段是类级别的,而非实例级别的。

通过这些机制,Java实现了对象的持久化和跨网络传输,使得对象能够方便地保存和恢复状态。

writeObject 和 readObject

writeObjectreadObject这两个方法是来自于Java的java.io.Serializable接口的实现,是用于自定义序列化和反序列化流程的方法。当一个类实现了Serializable接口后,Java默认使用默认的序列化机制,但是你可以在类中定义这两个方法以控制序列化和反序列化的行为。

方法来源和作用

  1. writeObject:

    • 这是一个保护级别的方法,它不是在Serializable接口中定义的。相反,它是从java.io.ObjectOutputStream类中继承的。
    • 在自定义类中,如果你重写这个方法,系统会在序列化该对象时调用此方法。通过这个方法,你可以明确指定在序列化过程中要写入的字段或进行一些特定操作。
    1
    2
    3
    4
    5
    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
    // 自定义序列化逻辑
    out.defaultWriteObject(); // 调用默认的序列化机制
    // 额外的序列化操作
    }
  2. readObject:

    • 这是一个保护级别的方法,来源于java.io.ObjectInputStream类。
    • 当反序列化一个对象时,JVM会调用该方法。你可以在这个方法中自定义反序列化的过程,以便在重建对象时执行额外的逻辑。
    1
    2
    3
    4
    5
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
    // 自定义反序列化逻辑
    in.defaultReadObject(); // 调用默认的反序列化机制
    // 额外的反序列化操作
    }

使用场景

  • 如果你需要在序列化或反序列化时做一些额外的处理,比如计算某些字段的值、记录日志、安全检查等,重写这两个方法是非常有用的。
  • 通过调用defaultWriteObject()defaultReadObject(),可以保留默认的行为,同时在此基础上添加自定义逻辑。

注意事项

  • 这两个方法必须标记为private,因为它们被Java的序列化机制调用,不应由外部直接调用。
  • 在反序列化的时候,确保使用ClassNotFoundException来处理类名未找到的异常。

总结起来,writeObjectreadObject方法允许程序员在序列化和反序列化过程中插入自定义逻辑,从而提供更大的灵活性。

Android 的 Parcelable

Android特有的序列化方案

Parcelable 在进程之间传递数据是使用了Binder


关于序列化常见面试问题

1.什么是 serialVersionUID ? 如果你不定义这个,会发生什么?

为确保反序列化的兼容性

serialVersionUID 是 Java 中用于序列化和反序列化机制的一个唯一标识符。
它是一个静态常量,通常被定义为 private static final long serialVersionUID 类型。
当一个类实现了 Serializable 接口时,JVM 会使用 serialVersionUID 来验证序列化对象的版本。

如果没有定义 serialVersionUID,Java 会根据类的结构自动生成一个值。
这样,如果你对类进行了修改(如添加、删除或更改属性),自动生成的 serialVersionUID 可能会发生变化,从而导致反序列化时出现 InvalidClassException。(已经序列化的对象无法恢复,不能反序列化)

因此,定义 serialVersionUID 的好处是:

  1. 版本控制:明确指定类的版本,有助于你管理序列化兼容性。
  2. 避免异常:修改类时不会意外改变自动生成的 serialVersionUID,从而降低了出错的可能性。

总之,虽然可以不定义 serialVersionUID,但建议在实现 Serializable 的类中明确指定,以确保序列化和反序列化过程的稳定性和兼容性。

2.假设你有一个类,它序列化并存储在持久性中,然后修改了该类以添加新字段如果对已序列化的对象进行反序列化, 会发生什么情况?

  1. 默认行为(没有定义 serialVersionUID):

    • 如果没有明确地定义 serialVersionUID,JVM 将根据类的当前结构自动生成一个新的 serialVersionUID。当你修改类(例如添加字段)后,自动生成的 serialVersionUID 会由于类的变化而不同,从而导致反序列化时抛出 InvalidClassException。这意味着反序列化操作失败,你将无法成功恢复该对象。
  2. 定义了 serialVersionUID

    • 如果在类中显式定义了一个不变的 serialVersionUID 字段(即在修改类时不改动这个值),当尝试反序列化之前序列化的对象时,反序列化操作将成功完成。
    • 在这种情况下,JVM 会成功加载对象的现有字段,对于新增的字段,会将它们初始化为该字段的默认值(例如,对于 int 类型的新字段,默认值为 0;对于 String 类型,默认值为 null)。

总的来说,为了确保序列化和反序列化的兼容性,建议在实现 Serializable 的类中显式定义 serialVersionUID。这样可以避免不必要的反序列化错误,同时允许你的类在添加新字段或进行其他修改时仍然能够反序列化旧版本的对象。

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

public transient String nickName; //不希望nickName序列化
transient 瞬态

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

  1. 瞬态(transient)变量:
  • 使用 transient 关键字修饰的变量在序列化过程中会被忽略,即不会被序列化。
  • 无论是在哪种情况下,标记为 transient 的字段都不会在序列化的输出中包含。
  1. 静态(static)变量:
  • static 变量是类级别的属性,而不是对象的实例属性。
  • 在序列化过程中,static 变量也不会被序列化。
  • 因为 static 变量与具体的对象实例无关,它们属于类本身,而不是类的实例。

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

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

5.如果类是可序列化的,但其超类(父类)不是,则反序列化后从超级类继承的实例变量的状态如何?

将会被初始化为默认值

6.是否可以自定义序列化过程, 或者是否可以覆盖 Java 中的默认序列化过程

java 允许你自定义序列化过程,并且可以覆盖默认的序列化和反序列化行为。
这通常是通过实现 writeObjectreadObject 方法来完成的。这样可以让你控制序列化时哪些数据被写入,以及反序列化时如何恢复对象的状态。

自定义序列化过程:

  1. 实现 writeObject 方法:
  • 通过实现此方法,你可以自定义对象在序列化时的行为。
  • 通常,在这个方法中使用 ObjectOutputStream 来写入想要序列化的字段。
  1. 实现 readObject 方法:
  • 通过实现此方法,你可以在对象反序列化时自定义恢复对象的状态。
  • 通常,在该方法中使用 ObjectInputStream 来读取序列化的数据。

7.假设新类(子类)的超级类实现可序列化接口,如何避免新类(子类)被序列化?

  1. 子类可以实现 Serializable 接口,但其所有字段都标记为 transient,这样这些字段在序列化时不会被保存
  2. 另一种方法是,让子类不实现 Serializable 接口,这样即使超类实现了,子类也是不可序列化的,这是最直接的方法。

8.在 Java 中的序列化和反序列化过程中使用哪些方法?

序列化

  1. writeObject(Object obj):
  • 使用 ObjectOutputStream 类的该方法来序列化对象。
  • 它将对象转换为字节流并写入输出流。
  1. defaultWriteObject():
  • 在自定义 writeObject 方法时,可以调用此方法来将非 transient 字段写入输出流。
  • 它负责序列化对象的所有可序列化字段。

反序列化

  1. readObject():
  • 使用 ObjectInputStream 类的该方法来反序列化对象。
  • 它从输入流中读取字节流并重建对象。
  1. defaultReadObject():
  • 在自定义 readObject 方法时,可以调用此方法来读取对象的非 transient 字段。
  • 它负责还原被序列化的字段。

面试常问

1. 反序列化后的对象,需要调用构造函数重新构造吗?

反序列化对象是直接从二进制流里面解析出来的,解析出来之后进行强转的
不会调用到构造函数

2. 序列化前的对象与序列化后的对象是什么关系? 是(“==”还是equal? 是浅复制还是深复制?)

序列化前的对象与序列化后的对象之间的关系是“equal”,即它们在逻辑上表示相同的数据。然而,序列化后的对象是以一种特定格式(如字节流)存储的,物理上并不是同一个对象,因此这不是引用相等(”==”)。所以,通常对它们进行比较时使用 equals() 方法。

序列化过程通常被认为是深复制。因为在序列化时,对象的所有状态信息(即所有字段的值)都会被转换并保存到序列化格式中。反序列化时会创建一个全新的对象实例。

对象前后的地址是不同的,是深复制(枚举是例外)

  • 浅复制:复制对象的引用,内嵌对象不被复制,两者共享内存中的对象。
  • 深复制:复制对象及所有内嵌对象,得到完全独立的副本,内存中所有对象都是新的。

3. Android里面为什么要设计出 Bundle 而不是直接用 Map 结构?

Bundle 是 Android 特意设计用于在组件之间传递数据的序列化容器,具有以下优点:

  • 类型安全:Bundle 通过将数据存储为键值对来提供对数据类型的明确支持,而 Map 在类型安全性上不如 Bundle。
  • 数据结构优化:Bundle 针对 Android 系统进行了优化,能够高效地在跨进程的情况下传递数据。而 Map 的实现可能没有这样的优化。
  • 轻量级:Bundle 是轻量级的数据结构,特别适合 Android 应用中的数据传递需求。
  • 直观性:Bundle 提供了一套简单的方法来存取数据,并且针对 Android 的组件(如 Activity 和 Fragment)进行了优化。

Bundle 内部使用了ArrayMap,其存储空间效率方面要比HashMap好,更省空间
Bundle 本来就是用来存储少量数据的

4. SerialVersionUID的作用是什么?

SerialVersionUID 是一个版本控制标识符,用于序列化和反序列化过程中确保类向后兼容性。
它的主要作用包括:

  • 版本控制:在反序列化时,Java 会检查 SerialVersionUID 是否一致以验证类的兼容性。如果不一致,将抛出 InvalidClassException,这意味着类的结构已经发生变化。
  • 避免错误:为每个可序列化的类定义一个 SerialVersionUID 可以有效地防止由于类的变化(如添加/删除字段)导致的序列化失败。

5. Android中 Intent/Bundle 的通信原理及大小限制

通信原理

  • Intent 是 Android 中一种用于不同组件(如 Activity、Service、BroadcastReceiver)之间进行通信的消息机制。它可以携带附加数据。
  • Bundle 是一种存储键值对的结构,通常用作 Intent 的附加数据部分。通过 Bundle,你可以将复杂数据(如 Parcelable 对象)打包在 Intent 中传递。

大小限制

  • 在 Android 中,Intent 的大小有限制,通常是 1MB 左右,尽管不同设备可能会略有不同。如果超过这个限制,可能会抛出 TransactionTooLargeException
  • 包含在 Bundle 中的数据也必须遵循 Intent 的限制。

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

Intent 在 Android 中设计为跨进程通信的机制,直接传递对象会面临多个问题:

  • 序列化/反序列化开销:直接传递对象可能会有引用不一致的问题,序列化机制确保数据能够安全地在不同进程中传递。
  • 数据完整性:使用序列化机制可以确保数据在传输过程中不会被意外更改,提高了数据的完整性和安全性。
  • 类型兼容性:通过序列化,可以为不同版本的组件提供数据兼容性。因此,使用序列化提供了一种安全、有效的方式来传递对象数据。

7. 序列化与持久化的关系和区别是什么?

关系

  • 序列化是持久化的一种形式,涉及将对象转换为一种可以存储或传输的格式(如字节流),以便稍后可以重构该对象。
  • 使用序列化可以将对象的状态保存到文件、数据库或通过网络传输,具有持久化的特性。

区别

  • 目的不同:序列化主要用于将对象转换为可存储的格式,而持久化则是指将数据长期存储以便在未来使用的过程。
  • 使用场景:序列化通常用于短期存储和传递数据,而持久化则包括将数据存储到数据库、文件、共享首选项等以供长期使用。

序列化是为了进程之间数据交换而设计的
持久化是为了将数据长期存储下来

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2023-2025 Annie
  • Visitors: | Views:

嘿嘿 请我吃小蛋糕吧~

支付宝
微信