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字段编译器就会给出警告⚠️,根据警告提示就可以自动生成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也可以被序列化。这时sexnameage这三个字段都会被序列化。

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

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

writeObjectreadObject这两个方法从名字可以看出来,就是用来读写对象的,在序列化过程中我们需要把对象信息通过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赋值为当前对象的namethis即表示当前对象。其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方式一致,也很好理解,就不过多解释了。了解其运行机制之后至于怎么用大家就可以脑洞大开了。

在上面了解到writeReplacereadResolve的访问修饰符为ANY-ACCESS-MODIFIER,及代表着可以是任意类型的权限修饰符,例如privateprotectedpublic。但是因为这两个方法主要的作用是用来处理当前类对象的序列化与反序列化,所以通常推荐使用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接口必须重启writeExternalreadExternal两个方法,其功能就是实现序列化和反序列化的过程。与Serializable中的writeObjectreadObject功能一样。另外使用Externalizable实现序列化需要提供一个public的无参构造函数,否则在反序列化的过程中抛出java.io.InvalidClassException: com.eonliu.sample.serialization.User; no valid constructor异常。

Serializable vs Externalizable

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

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

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

邮箱:[email protected]

Github: https://github.com/Eon-Liu

CSDN:https://blog.csdn.net/EonLiu

你可能感兴趣的:(Android进阶之路)