转载

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 也可以被序列化。这时 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

邮箱:eonliu1024@gmail.com

Github: github.com/Eon-Liu

CSDN: blog.csdn.net/EonLiu

原文  https://juejin.im/post/5e1d72546fb9a02ff112cefb
正文到此结束
Loading...