Java对象的序列化

基本概念
  • 什么是序列化和反序列化
    序列化就是将对象写入到IO流中,反序列化就是从IO流中恢复对象。
  • 为什么需要序列化
    序列化后的流,可用于持久化到磁盘,也可以用于网络传输。使得Java对象可以跨进程、跨主机使用。
  • 转成Json和XML算序列化吗
    Java对象转成字符串、Json、XML等其实也称为“序列化”,但与JVM提供的序列化功能不太一样,可以说序列化是一个比较抽象的概念,但本文主要指JVM的序列化。
如何序列化和反序列化
  • 实现Serializable接口
    序列化最常用的方式是实现Serializable接口,然后让客户端去序列化。
public class Teacher implements Serializable {
    private String name;
    private int age;

    public Teacher(String name, int age) {
        System.out.println("调用了构造方法!");
        this.name = name;
        this.age = age;
    }
	
    // getter方法
}
public class Client {
    public static void main(String[] args) {
        System.out.println("序列化:");
        try(ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("object.txt"))) {
            Teacher teacher = new Teacher("Tom", 53);
            System.out.println("序列化之前的hash:" + teacher.hashCode());
            out.writeObject(teacher);
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("反序列化:");
        try(ObjectInputStream input = new ObjectInputStream(
                new FileInputStream("object.txt"))) {
            Teacher teacher = (Teacher) input.readObject();
            System.out.println("Teacher的名字:" + teacher.getName());
            System.out.println("Teacher的年龄:" + teacher.getAge());
            System.out.println("序列化之后的hash:" + teacher.hashCode());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出结果:

序列化:
调用了构造方法!
序列化之前的hash:142666848
反序列化:
Teacher的名字:Tom
Teacher的年龄:53
序列化之后的hash:310656974

根据输出结果,我们可以总结出以下三点信息:

  1. 不用提供无参构造方法
  2. 反序列化时,不会调用构造方法
  3. 如果没有重写hashCode,反序列化后,hashCode是不一样的

至于为什么hashCode会不一样?因为Java默认的hashCode是一个内存地址指针。反序列化后,对象自然不在原来的内存地址上,所以hashCode会不一样。所以这里我们提倡要覆盖hashCode方法和equals方法。

  • transient关键字
    如果一个字段使用了transient关键字,那它就不会被序列化。在反序列化时,如果它的引用类型,值就是null,如果是基本类型,就是基本类型的默认值。这里我们尝试把age声明为transient的。会输出:
Teacher的年龄:0
  • 自定义序列化方法
    transient关键字虽然使用方便,但“可定制化”不强,比如我虽然不想序列化age字段,但希望它反序列化时默认值是18,这样的需求就得使用自定义序列化方法来实现。

主要有这样几个方法:

private void writeObject(java.io.ObjectOutputStream out) throws IOException;
private void readObject(java.io.ObjectIutputStream in) throws IOException,ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;

private Object writeReplace() throws ObjectStreamException;
private Object readResolve() throws ObjectStreamException;

通过重写writeObject与readObject方法,就可以实现自定义的序列化和反序列化。这里需要注意的是两个方法要有“对称性”,自己在重写这两个方法的时候,需要保证被序列化后,能够顺利地被反序列化。

示例代码:

private void writeObject(ObjectOutputStream out) throws IOException {
    out.defaultWriteObject();
    out.writeInt(age);
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    age = in.readInt();
}

PS: 这两个类是私有的,是在什么时候被调用,如何被调用的呢?

答案是通过反射。详情可见 ObjectOutputStream 中的 writeSerialData 方法,以及
ObjectInputStream 中的 readSerialData 方法。

除此之外,还可以使用writeReplace与readResolve实现更高程度地定制化。

  1. writeReplace:在序列化时,会先调用此方法,再调用writeObject方法。此方法可将任意对象代替目标序列化对象。
  2. readResolve:反序列化时替换反序列化出的对象,反序列化出来的对象被立即丢弃。此方法在readeObject后调用。

那readObjectNoData()方法用来干嘛的呢?详情可以看Serializable接口的注释。大意是:当序列化流不完整时,readObjectNoData()方法可以用来正确地初始化反序列化的对象。

  • 实现Externalizable接口
    Externalizable接口是继承自Serializable接口的。

使用Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;必须提供public的无参构造器,因为在反序列化的时候需要反射创建对象。

示例代码:

@Override
public void writeExternal(ObjectOutput out) throws IOException {
    StringBuffer reverse = new StringBuffer(name).reverse();
    out.writeObject(reverse);
    out.writeInt(age);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    this.name = ((StringBuffer) in.readObject()).reverse().toString();
    this.age = in.readInt();
}
常见问题
  • 如果不实现Serializable接口会怎样?
    如果不实现Serializable接口(这里包括也不实现Externalizable),客户端在序列化时,会报NotSerializableException异常。

  • 如果内部属性是引用类型,怎么办?
    如果要序列化的对象内部有引用类型的属性,那这个属性也必须实现序列化,否则同样会报NotSerializableException异常。

  • 子类实现序列化,父类不实现序列化
    子类实现序列化,父类不实现序列化,此时父类要实现一个无参数构造器,否则反序列化时会抛InvalidClassException异常。因为如果父类不实现序列化,反序列化时会调用父类的无参构造器。

// 父类:
public class Person {
    private String nationality;

    public Person(String nationality) {
        this.nationality = nationality;
    }
}

// 子类:
public class Teacher extends Person implements Serializable {
    private String name;
    private int age;



    public Teacher(String name, int age) {
        super("China"); // 调用父类的有参构造方法
        System.out.println("调用了构造方法!");
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

// 客户端:
public class Client {
    public static void main(String[] args) {
        System.out.println("序列化:");
        try(ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("object.txt"))) {
            Teacher teacher = new Teacher("Tom", 53);
            System.out.println("序列化之前的hash:" + teacher.hashCode());
            out.writeObject(teacher);
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("反序列化:");
        try(ObjectInputStream input = new ObjectInputStream(
                new FileInputStream("object.txt"))) {
            Teacher teacher = (Teacher) input.readObject();
            System.out.println("Teacher的名字:" + teacher.getName());
            System.out.println("Teacher的年龄:" + teacher.getAge());
            System.out.println("序列化之后的hash:" + teacher.hashCode());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出结果:

序列化:
调用了构造方法!
序列化之前的hash:1060830840
反序列化:
java.io.InvalidClassException: serialize.Teacher; no valid constructor
	at java.base/java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:159)
	at java.base/java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:864)
	at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2061)
	at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1594)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:430)
	at serialize.Client.main(Client.java:20)
  • 同一个对象多次序列化会发生什么?
    Java序列化同一对象,并不会将此对象序列化多次得到多个对象。Java会给每个序列化成功的对象一个“序列化编号”。当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。否则,直接输出编号。
out.writeObject(teacher); // 序列化一次

// 反序列化两次,会抛EOFException异常
Teacher teacher = (Teacher) input.readObject();
Teacher teacher2 = (Teacher) input.readObject();
Teacher teacher = new Teacher("Tom", 53);
out.writeObject(teacher); 
teacher.setAge(23)
out.writeObject(teacher); // 序列化两次

// 反序列化两次
Teacher teacher = (Teacher) input.readObject();
Teacher teacher2 = (Teacher) input.readObject();
System.out.println(teacher.equals(teacher2)); // true
System.out.println("Teacher的年龄:" + teacher.getAge()); // 53
  • 序列化和反序列化的顺序?
    反序列化时,取出对象的顺序与序列化是一致的。也就是说,先存先取,后存后取。
Teacher teacher1 = new Teacher("Tom", 53);
out.writeObject(teacher1);
Teacher teacher2 = new Teacher("Bob", 29);
out.writeObject(teacher2);

Teacher teacher1 = (Teacher) input.readObject();
System.out.println(teacher1.getName()); // Tom
Teacher teacher2 = (Teacher) input.readObject();
System.out.println(teacher2.getName()); // Bob
参考文章

java序列化,看这篇就够了
深度解析JAVA序列化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值