Java对象的序列化和反序列化源码阅读

简介: 前言序列化和反序列化看起来用的不多,但用起来就很关键,因为稍一不注意就会出现问题。序列化的应用场景在哪里?当然是数据存储和传输。比如缓存,需要将对象复刻到硬盘存储,即使断电也可以重新反序列化恢复。下面简单理解序列化的用法以及注意事项。

前言

序列化和反序列化看起来用的不多,但用起来就很关键,因为稍一不注意就会出现问题。序列化的应用场景在哪里?当然是数据存储和传输。比如缓存,需要将对象复刻到硬盘存储,即使断电也可以重新反序列化恢复。下面简单理解序列化的用法以及注意事项。

如何序列化

Java中想要序列化一个对象,必须实现Serializable接口。然后就可以持久化和反序列化了。下面是一个简单用法。

项目测试代码:
https://github.com/Ryan-Miao/someTest/blob/master/src/main/java/com/test/java/serial/TestSerialize.java

我们给一个测试类:

package com.test.java.serial;

import lombok.Builder;
import lombok.Data;

import java.io.Serializable;

/**
 * @author Ryan Miao
 */
@Data
@Builder
public class Foo implements Serializable {

    private static final String LOGGER = "logger";
    public static final String PUB_STATIC_FINAL = "publicStaticFinal";
    public static String PUB_STATIC;

    public String fa;
    private String fb;
    transient public String ta;
    transient private String tb;
}

然后,测试序列化和反序列的数据是否丢失。


public class TestSerialize {

    private static final String filename = "D:/test.txt";

    @Test
    public void testSer() throws IOException, ClassNotFoundException {
        final Foo foo = Foo.builder()
                .fa("fa")
                .fb("fb")
                .ta("ta")
                .tb("tb")
                .build();

        Foo.PUB_STATIC  = "test";

        ObjectOutputStream os = new ObjectOutputStream(
                new FileOutputStream(filename));
        os.writeObject(foo);
        os.flush();
        os.close();

    }

    @Test
    public void testRead() throws IOException, ClassNotFoundException {
        ObjectInputStream is = new ObjectInputStream(new FileInputStream(filename));
        Foo foo2 = (Foo) is.readObject();
        is.close();

        Assert.assertEquals("fa", foo2.getFa());
        Assert.assertEquals("fb", foo2.getFb());
        Assert.assertEquals(null, foo2.getTa());
        Assert.assertEquals(null, foo2.getTb());

        Assert.assertNull(foo2.PUB_STATIC);
    }
}

显然,transient修饰的字段不能被序列化,至于静态字段,这里不做测试,但要清楚。静态字段只和class类相关,和实例无关。而序列化是针对实例的,所以无所谓对比内容变化。那么,静态字段反序列化后数据是什么样子的呢?当然是类变量本身应该的样子。如果没有初始化,则是默认值, 本测试中的结果为null。

为什么可以序列化

我们只要实现了Serialiable就可以序列化,那么为什么呢?查看ObjectOutputStreamwriteObject方法。


// remaining cases
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());
    }
}

显然,只针对String,Enum以及Serializable做了处理,因此想要序列化必须要实现这个接口。当然,String和Enum也实现了Serializable。

如何自定义序列化,Java基础类库中的ArrayList等为什么用transient还能序列化

简单的对象,对于不想序列化的字段,只要声明为transient就好。而有时候,我想对部分字段处理后序列化。比如ArrayList中存储数据的transient Object[] elementData;。我们知道ArrayList是可以序列化的,根源就在于自定义这里了。下面跟踪ObjectOutputStream源码,知道自定义的执行部分就可以验证了。

入口: java.io.ObjectOutputStream#writeObject


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

然后,核心方法


private void writeObject0(Object obj, boolean unshared)
        throws IOException{
    boolean oldMode = bout.setBlockDataMode(false);depth++;
    try {
        //省略若干行
        for (;;) {
            // 省略若干行
            desc = ObjectStreamClass.lookup(cl, true);
            //省略若干行
        }
        //省略若干行
        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 {
            //....
        }
    } finally {
        depth--;
        bout.setBlockDataMode(oldMode);
    }
}

这里,显然可以看到真正的执行序列化代码是writeOrdinaryObject(obj, desc, unshared);。 但直接追踪进去发现里面有许多初始化的字段是在之前做的处理。因此,先卖个关子,看前面初始化的部分,只找到我们想要初始化的字段即可。

进入desc = ObjectStreamClass.lookup(cl, true);


static ObjectStreamClass lookup(Class<?> cl, boolean all) {
    //省略若干行
    if (entry == null) {
        try {
            entry = new ObjectStreamClass(cl);
        } catch (Throwable th) {
            entry = th;
        }
        //.....
    }
    //省略若干行
}

进入entry = new ObjectStreamClass(cl);这里就是真正的初始化地方,前面省略的代码是缓存处理,当然缓存使用的ConcurrentHashMap。


private ObjectStreamClass(final Class<?> cl) {
    //省略无数行以及括号
    writeObjectMethod = getPrivateMethod(cl, "writeObject",
                            new Class<?>[] { ObjectOutputStream.class },
                            Void.TYPE);
    readObjectMethod = getPrivateMethod(cl, "readObject",
                            new Class<?>[] { ObjectInputStream.class },
                            Void.TYPE);
    //省略无数行

没错,费了这么大劲就是为了找到这两个method。通过反射,获取到目标class的两个私有方法writeObject, readObject。这两个就是自定义方法所在。

初始化完毕之后,我们再来继续序列化的代码. 回到刚才的核心方法,找到writeOrdinaryObject(obj, desc, unshared);, 进入,然后,继续找到writeSerialData(obj, desc);, 到这里就是真正执行序列化的代码了。


private void writeSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
{
    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
    for (int i = 0; i < slots.length; i++) {
        ObjectStreamClass slotDesc = slots[i].desc;
        if (slotDesc.hasWriteObjectMethod()) {
            //....
            try {
                curContext = new SerialCallbackContext(obj, slotDesc);
                bout.setBlockDataMode(true);
                slotDesc.invokeWriteObject(obj, this);
                bout.setBlockDataMode(false);
                bout.writeByte(TC_ENDBLOCKDATA);
            } finally {
                //...
            }

            curPut = oldPut;
        } else {
            defaultWriteFields(obj, slotDesc);
        }
    }
}

显然,判断writeObject这个method是否初始化了,如果有,则直接调用这个方法,没有则默认处理。到此,跟踪完毕,我想要自定义序列化只要重写writeObject, readObject这两个方法即可。

下面看看ArrayList是怎么做的

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

因为数组被设置不允许序列化,先默认序列化其他信息,然后单独处理数组里的内容,挨着写入元素。然后,对应读取方法也要改。

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}

为什么要这么做?因为数组元素有很多空余空间,对我们来说不需要序列化。通过这样自定义,把需要的元素序列化,可以节省空间。

serialVersionUID为什么有的有,有的没有,什么时候用,意义是什么

以下内容来自: https://www.cnblogs.com/ouym/p/6654798.html

什么是serialVersionUID ?

serialVersionUID表示:“串行化版本统一标识符”(serial version universal identifier),简称UID

serialVersionUID必须定义成下面这种形式:static final long serialVersionUID = xxxL;

serialVersionUID 用来表明类的不同版本间的兼容性。有两种生成方式: 一个是默认的1L;另一种是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段 。

为什么要声明serialVersionUID

java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。 java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

只有实现了Serializable或Externalizable接口的类的对象才能被序列化。

Externalizable接口继承自Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式 。 凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:private static final long serialVersionUID;

类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的serialVersionUID。显式地定义serialVersionUID有两种用途:

  1. 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;在某些场合,不希望类的不同版本对序列化兼容,
    因此需要确保类的不同版本具有不同的serialVersionUID。
  2. 当你序列化了一个类实例后,希望更改一个字段或添加一个字段,不设置serialVersionUID,所做的任何更改都将导致无法反序化旧有实例,并在反序列化时抛出一个异常。
    如果你添加了serialVersionUID,在反序列旧有实例时,新添加或更改的字段值将设为初始化值(对象为null,基本类型为相应的初始默认值),字段被删除将不设置。

注意事项

  1. 序列化时,只对对象的状态进行保存,而不管对象的方法;
  2. 当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
  3. 当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
  4. 并非所有的对象都可以序列化,,至于为什么不可以,有很多原因了,比如:

    1. 安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行rmi传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的。
    2. 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分 配,而且,也是没有必要这样实现。

参考

    关注我的公众号

img_acfe50023e4718b33064273d65e4cd67.jpe
唯有不断学习方能改变! -- Ryan Miao
目录
相关文章
|
1月前
|
安全 Java 编译器
Java对象一定分配在堆上吗?
本文探讨了Java对象的内存分配问题,重点介绍了JVM的逃逸分析技术及其优化策略。逃逸分析能判断对象是否会在作用域外被访问,从而决定对象是否需要分配到堆上。文章详细讲解了栈上分配、标量替换和同步消除三种优化策略,并通过示例代码说明了这些技术的应用场景。
Java对象一定分配在堆上吗?
|
2月前
|
Java API
Java 对象释放与 finalize 方法
关于 Java 对象释放的疑惑解答,以及 finalize 方法的相关知识。
54 17
|
1月前
|
JSON 数据格式 索引
Python中序列化/反序列化JSON格式的数据
【11月更文挑战第4天】本文介绍了 Python 中使用 `json` 模块进行序列化和反序列化的操作。序列化是指将 Python 对象(如字典、列表)转换为 JSON 字符串,主要使用 `json.dumps` 方法。示例包括基本的字典和列表序列化,以及自定义类的序列化。反序列化则是将 JSON 字符串转换回 Python 对象,使用 `json.loads` 方法。文中还提供了具体的代码示例,展示了如何处理不同类型的 Python 对象。
|
1月前
|
存储 安全 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第22天】在Java的世界里,对象序列化和反序列化是数据持久化和网络传输的关键技术。本文将带你了解如何在Java中实现对象的序列化与反序列化,并探讨其背后的原理。通过实际代码示例,我们将一步步展示如何将复杂数据结构转换为字节流,以及如何将这些字节流还原为Java对象。文章还将讨论在使用序列化时应注意的安全性问题,以确保你的应用程序既高效又安全。
|
1月前
|
存储 缓存 NoSQL
一篇搞懂!Java对象序列化与反序列化的底层逻辑
本文介绍了Java中的序列化与反序列化,包括基本概念、应用场景、实现方式及注意事项。序列化是将对象转换为字节流,便于存储和传输;反序列化则是将字节流还原为对象。文中详细讲解了实现序列化的步骤,以及常见的反序列化失败原因和最佳实践。通过实例和代码示例,帮助读者更好地理解和应用这一重要技术。
48 0
|
1天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
3天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
3天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
3天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
15 3
|
3天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
36 2