深入理解Java和Android对象序列化以及反序列化

本文深入解析Java序列化和反序列化的概念与实现方法,包括Serializable、transient关键字、Externalizable接口及writeObject方法的使用技巧,并对比Android中Parcelable接口的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 什么是序列化和反序列化,首先看一下维基百科的定义:

在数据储存与传送的部分是指将一个对象存储至一个储存媒介,例如档案或是记亿体缓冲等,或者透过网络传送资料时进行编码的过程,可以是字节或是XML等格式。而字节的或XML编码格式可以还原完全相等的对象。程序被应用在不同应用程序之间传送对象,以及将对象储存到档案或数据库。相反的过程又称为反序列化。

简单的来说就是将java对象转变成字节序列或者xml格式的数据,进而来进行网络传输或者存储到文件中(也可以在内存中传输),从而达到持久化的目的(对象只有在jvm运行时才有,序列化就是考虑到jvm不在运行时也能保存对象)。之所以还有xml格式的序列化数据是因为考虑要兼容不同平台和语言(不同平台和语言都认识xml格式的文件);本篇博客我们只考虑在java和android中的序列化,所以只探讨java对象的字节序列化与反序列化。

2 举个栗子:
首先写一个实体类,实现了Serializable接口(java中,只要一个类实现了该接口,就可以被序列化),该接口不含任何方法和字段:

public class User implements Serializable {
    private String name;
    private int age;
    private Gender gender;

 public User(String name, String password, int age, Gender gender) {
        this.name = name;
        this.password = password;
        this.age = age;
        this.gender = gender;
        System.out.println("有参数构造函数");
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Gender getGender() {
        return gender;
    }

    public void setGender(Gender gender) {
        this.gender = gender;
    }
    @Override
    public String toString() {
        return "{ name: "+name+" passwd: "+password+" age: "+age+" gender: "+gender+" }";
    }}

其中Gender是枚举类型,因为枚举是继承类java.lang.Enum,该类实现了Serialiazble接口,所以枚举也可以被序列化;

public enum Gender  {
    Male("男"),Female("女");
    private String string;
    Gender(String sex){
        string = sex;
    }
    @Override
    public String toString() {
        return super.toString()+"("+string+")";
    }
}

接下来,我们编写一个场景类,用来将User对象保存到user.txt中,然后再从中读取出来,并打印看看。场景类如下:

public class SerializableTest {

    public static void main(String[] args) throws IOException,ClassNotFoundException{

        File file = new File("user.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
        User user = new User("Mike","123456",18,Gender.Male);
        objectOutputStream.writeObject(user);
        objectOutputStream.close();

        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
        User newUser = (User) objectInputStream.readObject();
        System.out.println(newUser);
System.out.println(user == newUser);
        objectInputStream.close();
    }
}

打印结果如下:
有参数构造函数
{ name: Mike passwd: 123456 age: 18 gender: Male(男) }
False
可以看到,User对象被顺利序列化了,并且也被顺利反序列化了,但是user和newUser两个对象并不是同一个值,只是他们的内容相同,这点要注意,此外,在反序列化时,并没有调用User类的构造函数。

3 Serialiazble作用

为啥实现了Serializable接口就可以被序列化呢,点击objectOutputStream.writeObject(user),我们看一下源码

public final void writeObject(Object obj) throws IOException {
        if (enableOverride) {
            writeObjectOverride(obj);
            return;
        }
        try {
            writeObject0(obj, false);
        } catch (IOException ex) {
            if (depth == 0) {
                /* ----- BEGIN android -----
                writeFatalException(ex);*/
                try {
                    writeFatalException(ex);

                } catch (IOException ex2) {                
            }
            throw ex;
        }
    }

然后接着点击writeObject0(obj, false);

private void writeObject0(Object obj, boolean unshared)
        throws IOException
    {
       //省略部分代码…
            if (obj instanceof Class) {
                writeClass((Class) obj, unshared);
            } else if (obj instanceof ObjectStreamClass) {
                writeClassDesc((ObjectStreamClass) obj, unshared);
            // ----- END android -----
            } else if (obj instanceof String) {
                writeString((String) obj, unshared);
            } else if (cl.isArray()) {
                writeArray(obj, desc, unshared);
            } else if (obj instanceof Enum) {
                writeEnum((Enum) obj, desc, unshared);
            } else if (obj instanceof Serializable) {
                writeOrdinaryObject(obj, desc, unshared);
            } else {
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }
        } finally {
            depth--;
            bout.setBlockDataMode(oldMode);
        }
    }

可以看到,如果对象是Class类型,String类型ObjectStreamClass类型,数组,枚举或者Serizable类型的话,就进行对象的序列化写入工作;当然,Class、String和ObjectStreamClass等类型也实现了Serizable接口,之所以还要分别判断一下,是因为他们的序列化写入方式不同,要分别处理,这点我们从源码也能看出来。

4 transient

假如说User类中有个字段我不想序列化,比如那个password字段,他保存的是用户的密码,这是敏感数据,如果序列化之后,万一泄漏了怎么办,这不要出事了,那我们怎么办呢,其实很简单,只需要在这个字段之前加transient关键字就可以了:
private transient String password;
那我们再来运行一下我们的场景类:打印如下:
{ name: Mike passwd: null age: 18 gender: Male(男) }
Passwd字段为null了,说明没有序列化进来。

5 Externalizable接口

Java还提供了一个Externalizable接口,让我们随意序列化对象,就是我们想序列那个字段就序列哪个字段,灵活度很高,该接口继承Serializable接口,并新增两个方法;
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
现在让我们的User类实现Externalizable接口,当然同时要覆写上面两个方法,

@Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeBytes(name);
        out.writeObject(gender);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = in.readLine();
        gender = (Gender) in.readObject();
}

同时,我们在User类中添加一个无参构造函数,否则编译不通过。

public User() {
System.out.println(“无参数构造函数”);
}
打印结果如下:
有参数构造函数
无参数构造函数
{ name: Mike passwd: null age: 0 gender: Male(男) }
False
可以看到,这种方式的反序列化中调用了无参的构造函数。自由度很高,想保持哪个字段就在上面两个方法里写。

6 writeObject()和readObject()

如果我们还是实现Serializable接口,也可以达到上面那种自由序列化的效果吗,答案是肯定,我们还让User实现Serializable,然后在User中添加两个方法,

private void writeObject(ObjectOutputStream out)throws IOException{
        //out.defaultWriteObject();//这句意思是按默认方法写,
        out.writeBytes(name);
        out.writeObject(gender);
    }
    private void readObject(ObjectInputStream in)throws IOException,ClassNotFoundException{
        //in.defaultReadObject();//这句意思是按默认方式读
        name = in.readLine();
        gender = (Gender) in.readObject();
}

7 serialVersionUID

一般在实体类中,我们最好再定义一个serialVersionUID字段,值可以随便写一个,比如:
private static final long serialVersionUID = 1L;
写这句有啥用呢,我举个例子,比如,我们的User类,之前已经被序列化到文件里,现在由于需求变更,需要在User类中再加一个字段userId,那么,加好之后,此时再去反序列化之前已经保存在文件里的对象,会抛出异常,就是因为java检测到现在的serialVersionUID与保存在文件里的serialVersionUID不一致,由于我们之前没有手动定义serialVersionUID这个字段,所以这个字段是java自己算的一个值,每次增加或者删除字段,这个值都会变化,如果,我们手动写死的话,这个值就不变了,也不会报异常了,这个读者可以自己实践一下。

至此,java的序列化与反序列化方式到此结束了,既然java已经提供了序列化方法,那安卓为啥也搞个自己的序列化方法呢,其实安卓的序列化主要是在内存中进行序列化,效率更高。下面我们先开一下安卓的序列化方式。

8 安卓的序列化方式,实现Parcelable接口

查阅谷歌官方文档,对该接口的解释是:
Interface for classes whose instances can be written to and restored from a Parcel. Classes implementing the Parcelable interface must also have a non-null static field called CREATOR of a type that implements the Parcelable.Creator interface.
意思是这个 接口是为那些可以写入Parcel以及从中恢复的类的实例来使用的,实现该接口的类必须有一个非空静态字段CREATOR,这个字段实现了Parcelable.Creator接口。
简单来说,Parcel提供了一套机制,可以将序列化的数据写到一个共享内存中,其他进程通过Parcel可以从这块共享内存中读出字节流,并反序列化成对象
再说明白点:parcel的中文意思是包裹,parcelable是可打包的意思,就是说,如果一个对象是可打包的(实现Parcelable接口),那么它就可以被打包进包裹里(parcel),而包裹可以在内存中传输,每个进程都认识,并且可以从包裹里拿出被打包的对象。
比如,如下:

User2 user = new User2("Tom","123456",88);
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        Parcel parcel = Parcel.obtain();
        parcel.writeParcelable(user,0);
        User2 newUser = parcel.readParcelable(User2.class.getClassLoader());
        Log.e("newuser",user.toString());
    }

下面我们试一下这个方法,新建一个User2类。并实现Parcelable接口:

public class User2 implements Parcelable{

    private String name;
    private  String password;
    private int age;

    public User2(String name, String password, int age) {
        this.name = name;
        this.password = password;
        this.age = age;
    }

public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeString(password);
        dest.writeInt(age);

    }
    public static final Creator<User2> CREATOR = new Creator<User2>() {
        @Override
        public User2 createFromParcel(Parcel source) {
            User2 user = new User2("","",0);
            user.name = source.readString();
            user.password = source.readString();
            user.age = source.readInt();
            return user;
        }

        @Override
        public User2[] newArray(int size) {
            return new User2[size];
        }
    };

    @Override
    public String toString() {
        return "{ name: "+name+" passwd: "+password+" age: "+age+" }";
    }
}

首先在writeToParcel方法里写上我们要序列化的对象,然后在createFromParcel方法里进行反序列化操作。这个是固定的套路。用起来就比较简单了:

在Activity中的页面跳转时一般这样用:
User2 user = new User2(“Tom”,”123456”,88);
Intent intent = new Intent(this,OtherActivity.class);
intent.putExtra(“user”,user);
startActivity(intent);

在另一个页面一般直接获取就行
User2 user = getIntent().getParcelableExtra(“user”);

总结:

  1. android在内存序列化上,应尽量使用parcelable方式,效率更高,将一个完整的对象进行分解,或者说他只是把对象的字段进行序列化了,反序列化的时候,先手动创建一个对象,然后给对象的字段分别进行赋值,这样效率更高。android的跨进程通信传输的对象就是实现parcelableh接口;Serialiable将整个对象序列化,使用了反射机制,效率略低,这种机制会在序列化的时候创建许多的临时对象,容易触发垃圾回收
  2. Serialiable方式实现起来较为简单,parcelable方式实现较为复杂,
  3. 序列化到存储设备上时,还使用Serialiable方式
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值