深入理解Java 序列化(一)

深入理解Java 序列化(一)

PS:本文章重点从Java的序列化体系出发进行学习(后期也会持续更新,不断完善)

内容借鉴张洪亮老师的 《深入理解Java核心核心技术》 - 第十二章:序列化


一、序列化和反序列化

在Java中,我们可以通过多种方式来创建对象,只要对象没被JVM回收,我们都可以复用该对象。但是,创建出来的这些对象都是存在于JVM堆中,也就是内存当中,一旦JVM停止运行,这些对象也就随之丢失了。

在真实的应用场景中,我们需要将这些对象持久化,并且在需要时重新读取对象,例如:数据库存储、网络传输RMIRPC都需要这样的操作)。

序列化是将对象转换为可存储或传输的形式的过程,一般都是以字节码或者XML、JSON等格式传输对象的。而将这些格式还原为对象的过程成为反序列化

Java内置了对象序列化机制(Object Serialization),这是Java内置的一种对象持久化方式,通过对象序列化,可以把对象状态保存为字节数组,并且在有需要时将这个字节数组通过反序列化的方式再转换为对象。对象序列化/反序列化可以很容易的在JVM中的活动对象和字节数组之间进行转换。

1、相关接口及类

  • java.io.Serializable
  • java.io.Externalizable
  • ObjectOutput
  • ObjectInput
  • ObjectOutPutStream
  • ObjectInputStream

2、Serializable接口

类通过实现Serializable接口以启用其序列化功能,未实现此接口的类将无法使用序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅仅用于标识可序列化的语义。

  • 当视图对一个对象进行序列化时,如果遇到不支持Serializable接口的对象,那么将抛出NotSerializableException
  • 如果序列化的类有父类,同时要持久化在父类中定义过的变量,那么父类也应该实现java.io.Serializable接口。

下面是一个实现了java.io.Serializable接口的类

/**
 * 实现Serializable接口
 *
 * @author baijiechong
 * @since 2023/5/6 23:47
 **/
public class User1 implements Serializable {

    private String name;

    private Integer age;

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

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

    @Override
    public String toString() {
        return "User1{" +
                "name='" + name + '\'' +
                ", age='" + age + '\'' +
                '}';
    }
}

通过下面的代码对User1对象进行序列化以及反序列化

/**
 * 使用java内置序列化机制实现对象的序列化以及反序列化
 *
 * @author baijiechong
 * @since 2023/5/6 23:49
 **/
public class SerializableDemo1 {

    private static final String FILE_PATH = "H:\\user\\desktop\\serialization.txt";

    public static void main(String[] args) {
        final User1 user1 = new User1();
        user1.setName("baijiechong");
        user1.setAge("22");
        //序列化
        SerializableDemo1 demo1 = new SerializableDemo1();
        demo1.serialization(user1);
        //反序列化
        demo1.deserialization();
    }

    /**
     * 序列化
     * @param user1 序列化的对象
     */
    public void serialization(User1 user1) {
        ObjectOutputStream oos = null;
        try {
            OutputStream outputStream = Files.newOutputStream(Paths.get(FILE_PATH));
            oos = new ObjectOutputStream(outputStream);
            oos.writeObject(user1);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            //org.apache.tomcat.util.http.fileupload.IOUtils
            IOUtils.closeQuietly(oos);
        }
    }

    /**
     * 反序列化
     */
    public void deserialization() {
        File file = new File(FILE_PATH);
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(Files.newInputStream(file.toPath()));
            User1 user1 = (User1) ois.readObject();
            System.out.println(user1);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(ois);
            try {
                //org.apache.tomcat.util.http.fileupload.FileUtils
                FileUtils.forceDelete(file);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

执行后的输出结果为:

User1{name='baijiechong', age='22'}

另外如果在反序列化前,也就是 demo1.deserialization();这行代码打上断点,可以发现序列化后的字节码文件,打开文件及如下

aced 0005 7372 0022 636f 6d2e 7374 7564
792e 7365 7269 616c 697a 6162 6c65 2e6d
6f64 656c 2e55 7365 7231 5621 5096 a96b
b379 0200 024c 0003 6167 6574 0013 4c6a
6176 612f 6c61 6e67 2f49 6e74 6567 6572
3b4c 0004 6e61 6d65 7400 124c 6a61 7661
2f6c 616e 672f 5374 7269 6e67 3b78 7073
7200 116a 6176 612e 6c61 6e67 2e49 6e74
6567 6572 12e2 a0a4 f781 8738 0200 0149
0005 7661 6c75 6578 7200 106a 6176 612e
6c61 6e67 2e4e 756d 6265 7286 ac95 1d0b
94e0 8b02 0000 7870 0000 0016 7400 0b62
6169 6a69 6563 686f 6e67 

3、Externalizable接口

除了Serializable接口,Java还提供了另一个序列化接口Externalizable

为了了解Externalizable接口和Serializable接口的区别,我们把上面的代码改成使用Externalizable接口的形式:

/**
 * 实现Externalizable接口
 *
 * @author baijiechong
 * @since 2023/5/6 23:47
 **/
public class User2 implements Externalizable {

    private String name;

    private Integer age;

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

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

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        //nothing to do
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        //nothing to do
    }
    
    @Override
    public String toString() {
        return "User2{" +
                "name='" + name + '\'' +
                ", age='" + age + '\'' +
                '}';
    }   
}

通过下面的代码对User2对象进行序列化以及反序列化

/**
 * 直接对实现了Externalizable接口的类进行持久化
 *
 * @author baijiechong
 * @since 2023/5/7 0:28
 **/
public class ExternalizableDemo1 {

    private static final String FILE_PATH = "H:\\user\\desktop\\serialization.txt";

    public static void main(String[] args) {
        User2 user2 = new User2();
        user2.setName("baijiechong");
        user2.setAge(23);
        ExternalizableDemo1 externalizableDemo1 = new ExternalizableDemo1();
        //序列化
        externalizableDemo1.externalization(user2);
        //反序列化
        externalizableDemo1.deExternalization();
    }

    /**
     * 序列化
     *
     * @param user2 序列化的对象
     */
    public void externalization(User2 user2) {
        ObjectOutputStream oos = null;
        try {
            OutputStream outputStream = Files.newOutputStream(Paths.get(FILE_PATH));
            oos = new ObjectOutputStream(outputStream);
            oos.writeObject(user2);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            //org.apache.tomcat.util.http.fileupload.IOUtils
            IOUtils.closeQuietly(oos);
        }
    }

    /**
     * 反序列化
     */
    public void deExternalization() {
        File file = new File(FILE_PATH);
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(Files.newInputStream(file.toPath()));
            User2 user2 = (User2) ois.readObject();
            System.out.println(user2);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(ois);
            try {
                FileUtils.forceDelete(file);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

执行后的输出结果为:

User2{name='null', age='null'}

通过上面的实例可以发现,对User2类进行序列化以及反序列化后得到对象的所有字段值都变成了默认值。也就是说,User2对象的状态并没有被持久化,这就是Exernalizable接口和Serializable接口的区别:Externalizable接口继承了Serializable接口,该接口中定义了writeExternal()readExternal()两个抽象方法。

当使用Externalizable接口进行序列化和反序列化时,我们需要重写writeExternal()readExternal()方法。

由于上面的代码在这两个方法中没有定义序列化的细节,所以输出内容为空。

还有一点值得注意:在使用Externalizable接口进行序列化时,在读取对象时,会调用序列化类的无参构造器去创建一个新的对象,然后将保存对象的字段值分别填充到新对象中,所以实现了Externalizable接口的类必须提供一个public的无参构造方法。

按照要求更改User2的代码如下:

/**
 * 实现Externalizable接口
 *
 * @author baijiechong
 * @since 2023/5/6 23:47
 **/
public class User2 implements Externalizable {

    private String name;

    private Integer age;

    //无参构造 如果User2类中没有无参构造函数,那么再运行时会抛出java.io.InvalidClassException
    public User2() {
    }

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

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

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

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = (String) in.readObject();
        age = (Integer) in.readObject();
    }

    @Override
    public String toString() {
        return "User2{" +
                "name='" + name + '\'' +
                ", age='" + age + '\'' +
                '}';
    }
}

执行后的输出结果为:

User2{name='baijiechong', age='23'}

这样就把User2对象的状态持久化下来了。


二、什么是transient

如果看过ArrayList或者Vector的源码,会发现它们虽然都是使用数组实现的,但是在定义数组时稍微有些不同,那就是ArrayList使用了transient关键字

private transient Object[] elementData;  //ArrayList

protected Object[] elementData;          //Vector

下面看一看transient的关键字的作用是什么?

transientJava的关键字、变量修饰符、如果用transient声明一个实例变量,那么当对象存储时,它的值不需要维持

这里的对象存储是指Javaserialization提供的一种持久化对象实例的机制,当一个对象被序列化时,transient修饰的变量值不包括在序列化范围内,然而非transient的变量是被包括进去的。

使用情况是:当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serializable机制来保存它,为了在一个特定对象的一个域上关闭serializable,可以在这个域前加上关键字transient

简单的说,就是被transient修饰的成员变量,在序列化时时会被忽略,在被反序列化后,transient变量的值被设置为初始值。如int的初始值为0,对象型为null。

三、序列化底层原理

在了解了如何使用Java中的序列化之后,我们深入分析下Java序列化以及反序列化的原理。为了方便理解,我们围绕ArrayList来展开介绍Java是如何实现序列化以及反序列化的。

在介绍ArrayList之前,我们先思考一个问题,如何自定义序列化和反序列化呢?

带着这个问题我们看下java.util.ArrayList的源码
在这里插入图片描述

上面的代码中忽略了其他成员变量,ArrayList实现了serializable接口,其中elementData被定义为transient类型,而被transient修饰的成员变量不会被序列化留下来。

我们写一个demo验证一下猜想:

public static void main(String[] args) {
    String filePath = "H:\\user\\desktop\\ArrayList - serializable.txt";
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    try {
        //序列化
        OutputStream ops = Files.newOutputStream(Paths.get(filePath));
        ObjectOutputStream oops = new ObjectOutputStream(ops);
        oops.writeObject(list);
        IOUtils.closeQuietly(ops);
        IOUtils.closeQuietly(oops);
        
        //反序列化
        InputStream is = Files.newInputStream(Paths.get(filePath));
        ObjectInputStream ois = new ObjectInputStream(is);
        List<Integer> newList = (List<Integer>) ois.readObject();
        System.out.println(newList);
        IOUtils.closeQuietly(ops);
        IOUtils.closeQuietly(oops);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
init list :[1, 2, 3]
new list :[1, 2, 3]

从结果我们可以看出,集合中的列表都正常的进行了序列化和反序列化,核心数组elemetnDatatransient修饰的,按道理说elementData是不会被序列化的,而且在反序列化时应该赋值为null才对。那为什么上面的代码却把集合中的元素保留下来了呢?

1、writeObject 和 readObject 方法

ArrayList中定义了两个方法:writeObject()readObject(),这里先给出结论:

在序列化过程中,如果被序列化的类中定义了writeObject()readObject()方法,那么虚拟机会试图调用对象类中的writeObject()readObject()实现进行用户自定义的序列化和反序列化。

如果没有这两个方法,则默认调用的是ObjectOutPutStreamdefaultWriteObject()方法和ObjectInputStreamdefaultReadObject()方法。

用户自定义的writeObject()readObject()方法允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值,很灵活。

下面看看ArrayList中这两个方法的具体实现:
在这里插入图片描述在这里插入图片描述

为什么ArrayList要用这种方式来实现序列化呢?

2、为什么使用transient

我们都知道ArrayList实际上是动态数组,每次放入的元素达到阈值后,自动增长设置的长度值,如果数组自动增长的长度设为100,而实际之存放1个元素,那么就会序列化99个null元素,为了保证不对null元素进行序列化,所以ArrayList把元素数组elementData设置为transient


3、为什么重写writeObject和readObject

前面说过,为了防止包含大量空对象的数组被序列化,以及优化存储、速度,ArrayList使用transient来修饰elementData

但是,作为一个集合,在序列化过程中还必须保证其中不为null的元素可以被持久化下来,所以通过重写writeObject()readObject()方法的方法把其中的元素保留下来。

  • writeObject()方法把elementData数组中的元素遍历保存到输出流(ObjectOutPutStream)中。
  • readObject()方法从输入流(ObjectInputStream)中读取出对象并保存赋值到elementDtat中从而还原数组的状态。

至此,我们回答上面的问题:如何自定义序列化和反序列化的策略。

答:可以在被序列化的类中重写writeObject()和readObject()方法

问题又来了:

虽然ArrayList中写了writeObject()方法和readObject()方法,但是这两个方法并没有被显式的调用,那readObject和writeObject是何时被调用的呢?

4、ObjectOutPutStream

对象的序列化过程是通过ObjectOutPutStreamObjectInputStream实现的,带着刚才的问题,我们分析一下ArrayList中的writeObject()readObject()方法到底是如何被调用的。

这里直接给出ObjectOutPutStreamwriteObject()方法的调用栈:

wriiteObject() --->  writeObject0() ---> writeOrdinaryObject() ---> writeSerialData() ---> invokeWriteObject()

在这里插入图片描述

其中writeObjectMethod.invoke(obj,new Object[]{ out })是关键,通过反射的方式调用writeObjectMethod()方法。

官方的解释是这样的:

class-defined writeObject method , or null if none // 类定义的writeObject方法,如果没有则为null

所以说,这个方法就是ArrayList中定义的writeObject()方法,然后通过反射的方式被调用了。

至此,我们回答刚提出的问题:

如果一个类中包含writeObject和readObject方法,那么这两个方法是怎么被调用的呢?

:在使用ObjectOutPutStreamwriteObject()ObjectInputStreamreadObject()方法时,会通过反射的方式调用。

有的小伙伴可能会有疑问?

Serializable明明就是一个空接口,它是怎么保证只有实现了该接口的方法才能进行序列化与反序列化呢?

Serializable接口的定义如下:

public interface Serializable {
    
}

当尝试对一个为实现Serializable或者Externalizable接口的对象进行序列化时,会抛出java.io.NotSerializableException异常。

其实这个问题也很简单,我们再回到刚刚writeObject()方法的调用栈:

wriiteObject() --->  writeObject0() ---> writeOrdinaryObject() ---> writeSerialData() ---> invokeWriteObject()

writeObject()方法中有如下一段代码:

在这里插入图片描述

在进行序列化时,会判断对象是否为String、Array、EnumSerializable类型,如果都不是则直接抛出NotSerializableException异常。


四、总结

  • 如果一个类想被序列化,则必须实现Serializable接口,否则将抛出NotSerializableException异常,这是因为在序列化操作过程中会对类的类型进行检查,要求被序列化的类必须属于String、Array、EnumSerializable中的一种。
  • 在变量声明前加上关键字transient,可以阻止该变量被序列化到文件中,同时也可以参考ArrayList为了不序列化无意义对象的做法,就是 transient + 自定义writeObject()
  • 在类中增加writeObject()readObject()方法可以实现自定义序列化和反序列化的策略。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值