Android进阶之路——Serializable序列化

简介

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

在Android中序列化最多见的使用场景就是缓存数据了。如今的App中基本须要缓存数据,例如缓存用户登陆信息。git

// 用来保存用户信息
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接口就能够了。github

Serializable

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

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

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

若是想要使User可被序列化只要实现Serializable接口便可。ide

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字段了。this

总结:加密

  • 尽可能显示声明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

相关文章
相关标签/搜索