Android进阶之路——Serializable序列化

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。——百度百科。

Android中序列化最常见的使用场景就是缓存数据了。现在的App中基本需要缓存数据,例如缓存用户登录信息。

// 用来保存用户信息
public class User {
    private String name;
    private int age;
    
    // getter/setter
}

// 用户信息
User user = new User("Eon Liu", 18);
ObjectOutputStream oos = null;
try {
    // 缓存路径(需要开启存储权限)
    File cache = new File(Environment.getExternalStorageDirectory(), "cache.txt");
    oos = new ObjectOutputStream(new FileOutputStream(cache));
    // 将用户信息写到本地文件中
    oos.writeObject(user);
} catch (IOException e) {
    e.printStackTrace();
} finally {
    // 关闭流
    if (oos != null) {
        try {
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
复制代码

通常在登录成功之后我们将用户的信息解析成一个类似 User
的对象,然后将其保存在 SDCard
中。这一过程就需要用到序列化。上面代码我们并没有对 User
进行可序列化的处理,所以在保存过程中就会抛出 java.io.NotSerializableException: com.eonliu.sample.serialization.User
这样的Java异常。因为在 writeObject
方法中对需要存储的类进行了校验,如果没有实现 Serializable
接口就会抛出这个异常信息。处理这种异常也很简单,只要使 User
类实现 Serializable
接口就可以了。

Serializable

Serializable是Java中提供的序列化接口。

package java.io;
public interface Serializable {
}
复制代码

Serializable
是一个空接口,它仅仅是用来标识一个对象是可序列化的。

如果想要使 User
可被序列化只要实现 Serializable
接口即可。

public class User implements Serializable {

    private static final long serialVersionUID = 8279379322154244252L;
    private String name;
    private int age;
    
    // getter/setter
}

复制代码

可以看到User类实现了 Serializable
接口,这时 User
就可以被序列化了。并且还多了一个 serialVersionUID
字段。那么这个字段是干什么用的呢?

serialVersionUID的作用及注意事项

serialVersionUID
是用来标记 User
类版本用的。其声明的格式是 任意访问权限修饰符 static final long serialVersionUID = longValue;
因为其作用是标识每个类的版本,所以最好使用 private
控制 serialVersionUID
的访问权限仅在当前类有用,不会被其他子类继承使用。

如果不显示声明 serialVersionUID
那么JVM会根据类的信息生成一个版本号,由于不同的JVM生成的版本号的能不一致,类的结构也可能发生变化等这些因素都可能导致序列化时候的版本号和反序列化时的版本号不止一次导致运行时抛出 InvalidClassException异常
。所以最佳实践还是在序列化时显示的指定 serialVersionUID
字段。其值是一个 long
类型的数值。这个值在 Android Studio
中默认是不能自动生成的,可以打开 Perferences-Editor-Code Style-Inspections-Serialization issues-Serializable class without serialVersionUID
,这样在实现 Serializable
接口是如果没有声明 serialVersionUID
字段编译器就会给出警告:warning:,根据警告提示就可以自动生成 serialVersionUID
字段了。

总结

  • 尽量显示声明 serialVersionUID
    字段。
  • 最好使用 private
    修饰 serialVersionUID
    字段。
  • 尽量使用 Android Studio
    或者其他工具生成 serialVersionUID
    的值。
  • 不同版本的类的 serialVersionUID
    值尽量保持一致,不要随意修改,否则反序列化时会抛出 InvalidClassException
    异常,反序列化失败。

不可被序列化的字段

有时候可能要序列化的对象中存在某些字段不需要被序列化。例如用户密码,为了保证安全我们不需要将密码字段进行序列化,那如何能做到这一点呢?实现 Serializable
接口时静态变量(被 static
修饰的变量)不会被序列化、另外被 transient
关键字修饰的变量也是不会被序列化的。

public class User implements Serializable {

    private static final long serialVersionUID = 8279379322154244252L;

    private String name;
    private int age;
    private transient String password;
    
    // getter/setter
}
复制代码

因为静态变量不能被序列化,所以 serialVersionUID
需要声明为 static
的,另外 password
被声明为 transient
也不会被序列化。

静态成员返回序列化时会取内存中的值,被 transient
修饰的成员变量使用其类型的默认值,例如 password
的默认值则为 null

继承或组合关系中的序列化

public class Person {
    private boolean sex;
    
    // getter/setter
}

public class User extends Person implements Serializable {
    private static final long serialVersionUID = 8279379322154244252L;
    private String name;
    private int age;
    private transient String password;
    
    // getter/setter
}
复制代码

父类 Person
没有实现 Serializable
接口,单其子类实现了 Serializable
接口,所以父类的信息不回被序列化,当我们保存 User
信息时,父类的 sex
字段是不会被保存的。反序列化时 sex
会使用 boolean
类型的默认值 false

另外当父类没有实现 Serializable
接口时,必须有一个可用的无参数构造函数,例如上面的 Person
代码并没有显示声明构造,JVM会生成一个无参数构造函数,但是如果我们将其代码改成如下形式:

public class Person {

    private boolean sex;

    public Person(boolean sex) {
        this.sex = sex;
    }
    
    // getter/setter
}
复制代码

这里显示声明了 Person
的构造函数,其参数为 sex
,这也是 Person
的唯一构造函数了。因为根据Java机制,当显示声明构造函数时JVM就不会生成无参数的构造函数。这样就会导致反序列化时候无法构造 Person
对象,抛出 java.io.InvalidClassException: com.eonliu.sample.serialization.User; no valid constructor
异常。

我们对上面的代码稍作修改。

当父类实现了 Serializable
接口时,其子类也可以被序列化。

public class Person implements Serializable {
    private static final long serialVersionUID = 2622760185052917383L;
    private boolean sex;
    
    // getter/setter
}

public class User extends Person {
    private static final long serialVersionUID = 8279379322154244252L;
    private String name;
    private int age;
    private transient String password;
    
    // getter/setter
}
复制代码

当父类 Person
实现了 Serializable
接口时,则子类 User
也可以被序列化。这时 sex
name
age
这三个字段都会被序列化。

还有一种情况就是当我们序列化的类中有一个成员变量是一个自定义类的情形。

public class Car {
    private String product;
    
    // getter/setter
}

public class Person implements Serializable {
    private static final long serialVersionUID = 2622760185052917383L;
    private boolean sex;
    
    // getter/setter
}

public class User extends Person {
    private static final long serialVersionUID = 8279379322154244252L;
    private String name;
    private int age;
    private transient String password;
    private Car car;
    // getter/setter
}
复制代码

User
中有一个成员变量为 Car
类型,因为 Car
没有实现 Serializable
接口,所以会导致 User
序列化失败,抛出 java.io.NotSerializableException: com.eonliu.sample.serialization.Car
异常,这时解决办法有两个,一个是使用 transient
修饰 Car
字段,使其在序列化时被忽略。另一个办法就是 Car
实现 Serializable
接口,使其拥有可序列化功能。

总结:

  • 继承关系中,父类实现 Serializable
    接口,则父类和子类都可被序列化。

  • 集成关系中,父类没有实现 Serializable
    接口,则父类信息不会被序列化,子类实现 Serializable
    接口则只会序列化子类信息。

  • 如果被序列化的类中有Class类型的字段则这个Class需要实现 Serializable
    接口,否则序列化时候回抛出“java.io.NotSerializableException 异常。或者使用
    transient`将其标记为不需要被序列化。

  • 如果父类没有实现 Serializable
    接口,则必须要有一个可用的无参数构造函数。否则抛出 java.io.InvalidClassException: com.eonliu.sample.serialization.User; no valid constructor
    异常。

自定义序列化过程

Serializable
接口预留了几个方法可以用来实现自定义序列化过程。

private void writeObject(java.io.ObjectOutputStream out)throws IOException
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException
复制代码

上面五个方法就是Java序列化机制中可以用来干预序列化过程的五个方法,他们具体能感谢什么继续往下看。

writeObject&readObject

writeObject
readObject
这两个方法从名字可以看出来,就是用来读写对象的,在序列化过程中我们需要把对象信息通过 ObjectOutputStream
保存在存储介质上,反序列化的时候就是通过 ObjectInputStream
从存储介质上将对象信息读取出来,然后在内存中生成一个新的对象。这两个方法就可以用来定义这一过程。

// 序列化
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
    // 写入性别信息(sex是Person的字段信息)
    out.writeBoolean(isSex());
    // 写入年龄信息
    out.writeInt(age);
}
// 反序列化
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
    // 恢复性别信息
    setSex(in.readBoolean());
    // 恢复年龄信息
    age = in.readInt();
}
复制代码

首先这两个方法要成对出现,否则一个都不要写。在 readObject
中read的次序要与在 writeObject
中write的次序保持一致,否则可能会导致反序列化的数据出现混乱的现象。另外我们这两个方法不关心父类是否实现了 Serializable
接口,如上面代码所示, out.writeBoolean(isSex());
中的 sex
字段就是来自父类 Person
的,即使 Person
没有实现 Serializable
接口这个序列化也会正常运行。

如果不需要自定义过程可以使用 out.defaultWriteObject();
来实现默认的序列化过程,使用 in.defaultReadObject();
实现默认的反序列化过程。

重写这两个方法可以自定义序列化和反序列的过程、例如可以自己定义那些字段可以序列化,哪些不被序列化,也可以对字段进行加密、解密的操作等。如果使用默认的序列化、反序列化的过程我们也可以在其过程的前后插入其他的逻辑代码来完成其他的任务。

readObjectNoData

readObjectNoData
主要是用来处理当类发生结构性的变化时处理数据初始化的,这么说可能有点抽象,我们还拿上面的案例来说明。

public class User implements Serializable {

    private static final String TAG = "SerializationActivity";
    private static final long serialVersionUID = -5795919384959747554L;
    private String name;
    private int age;
    private transient String password;
    private Car car;

    // getter/setter
}
复制代码

第一版本 User
类如上所示,这时候序列化 User
对象将其保存在 SDCard
上了,然后发现 User
取消性别字段,无法满足需求,于是就有了下一版。

public class Person implements Serializable {
    private static final long serialVersionUID = -3824243371733653209L;
    private boolean sex;

    ...
}

public class User extends Person implements Serializable {
	...
}
复制代码

在第二版本中 User
类继承了 Person
,同时也有用了性别的属性。此时 User
相对于第一版本中缓存的数据发生了结构性的变化,当使用第二版的 User
反序列化第一版的 User
信息时父类 Person
中的 sex
就没办法初始化了,只能使用 boolean
类型的默认值,也就是 false
了。那如何才能在反序列化过程中修改 sex
的值呢?就可以通过 readObjectNoData
方法来完成。

当反序列化过程中类发生了结构性的变化时 readObjectNoData
方法就会被调用,解决上面的问题我们就可以在 Person
中重写 readObjectNoData
方法来对 sex
进行初始化操作。

private void readObjectNoData() throws ObjectStreamException {
    sex = true;
}
复制代码

writeReplace

writeReplace
方法会在 writeObject
方法之前被调用,它返回一个 Object
,用来替换当前需要序列化的对象,并且在其内部可以用 this
来调用当前对象的信息。

// 返回值Object则是真正被序列化的对象
private Object writeReplace() throws ObjectStreamException {
    // 新创建一个User对象
    User user = new User();
    // 新User的name为当前对象的name值
    user.name = this.name;
    // 新User的age为20
    user.age = 20;
    // 返回新User对象
    return user;
}
复制代码

上面重写了 writeReplace
方法,并新建一个 User
对象,其 name
赋值为当前对象的 name
this
即表示当前对象。其 age
赋值为20,然后返回新的 user
对象,之后 writeObject
方法就会被调用,将在 writeReplace
方法中返回的 user
对象进行序列化。在反序列化中的得到 user
信息与 writeReplace
方法中新建的 user
信息一致。

writeReplace
方法中我们可以对其对象信息做一些过滤或者添加,甚至可以返回其他类型的对象都是可以的。只不过反序列化的过程也要做响应的转换。

readResolve

readResolve
方法会在 readObject
方法之后调用,返回值也是 Object
,它表示反序列化最终的对象。在其方法内部可以使用 this
表示最终反序列化对象。

private Object readResolve() throws ObjectStreamException {
    User user = new User();
    user.name = this.name;
    user.age = 20;
    return user;
}
复制代码

这里的实现代码与 writeReplace
方式一致,也很好理解,就不过多解释了。了解其运行机制之后至于怎么用大家就可以脑洞大开了。

在上面了解到 writeReplace
readResolve
的访问修饰符为 ANY-ACCESS-MODIFIER
,及代表着可以是任意类型的权限修饰符,例如 private
protected
public
。但是因为这两个方法主要的作用是用来处理当前类对象的序列化与反序列化,所以通常推荐使用 private
修饰,以防止其子类重写。

Externalizable

Externalizable
是Java提供的一个 Serializable
接口扩展的接口。

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

使用也很简单,与 Serializable
类似。

public class User implements Externalizable {
    private static final long serialVersionUID = -5795919384959747554L;
    private String name;
    private int age;
    
    ...
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        Log.d(TAG, "writeExternal: ");
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws ClassNotFoundException, IOException {
        Log.d(TAG, "readExternal: ");
        age = in.read();
    }
}
复制代码

Serializable
的去别就是实现 Externalizable
接口必须重启 writeExternal
readExternal
两个方法,其功能就是实现序列化和反序列化的过程。与 Serializable
中的 writeObject
readObject
功能一样。另外使用 Externalizable
实现序列化需要提供一个 public
的无参构造函数,否则在反序列化的过程中抛出 java.io.InvalidClassException: com.eonliu.sample.serialization.User; no valid constructor
异常。

Serializable vs Externalizable

Serializable
Externalizable
都可以实现序列化,那么他们有什么区别呢?该如何选择呢?

  • Serializable
    只是标记接口,其序列化过程都交给了JVM处理,使用相比 Externalizable
    更简单。
  • Externalizable
    并不是标记接口,实现它就必须重写两个方法来实现序列化和反序列化,相对复杂一点。
  • 由于 Serializable
    把序列化和反序列化的过程都交给了JVM,所以在个别情况可能其效率不如 Externalizable

所以通常情况下使用 Serializable
来实现序列化和反序列化过程即可。只有充分的了解到使用 Externalizable
实现其序列化和反序列化会使其效率有所提升才或者需要完全自定义序列化和反序列化过程才考虑使用 Externalizable

邮箱:eonliu1024@gmail.com

Github: github.com/Eon-Liu

CSDN: blog.csdn.net/EonLiu

原文 

https://juejin.im/post/5e1d72546fb9a02ff112cefb

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » Android进阶之路——Serializable序列化

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址