Java序列化和反序列化机制

这篇具有很好参考价值的文章主要介绍了Java序列化和反序列化机制。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Java的序列化和反序列化机制

问题导入:

在阅读ArrayList源码的时候,注意到,其内部的成员变量动态数组elementData被Java中的关键字transient修饰

transient关键字意味着Java在序列化时会跳过该字段(不序列化该字段)

而Java在默认情况下会序列化类(实现了Java.io.Serializable接口的类)的所有非瞬态(未被transient关键字修饰)和非静态('未被static关键字修饰')字段

为什么ArrayList要给非常重要的动态数组成员变量elementData添加transient关键字?

事实上,ArrayListelementData添加transient关键字的原因是因为Java默认的序列化方法并不理想

  • 空间效率: 由于扩容机制,elementData数组的容量可能会大于实际存储的元素数量,数组中可能存在未使用的空间,如果直接走Java默认的序列化,直接序列化整个数组,会将这部分未使用的空间也一起序列化,导致空间浪费
  • 控制序列化行为: 通过自定义writeObject()readObject()方法,ArrayList能够更好地控制序列化和反序列化过程,仅序列化实际包含的元素,并在反序列化时重新创建合适的数组大小

那么,Java的序列化机制,标识接口Java.io.Serializable和关键字transient等是如何运作的?

从两个类说起

Java中实现序列化和反序列化的两个核心类是ObjectInputStreamObjectOutputStream

  • ObjectOutputStream:将Java对象的原始数据类型以流的方式写出到文件,实现对象的持久化存储
  • ObjectInputStream:将文件中保存的对象,以流的方式取出来使用

一个简单的示例

//1.创建一个类 实现序列化接口(标识该类可被序列化,如果不实现该接口,调用序列化方法会报java.io.NotSerializableException)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Serializable {

    private String name;

    private Integer age;

    //标记remark字段 不会被序列化
    private transient String remark;

}
//2.序列化和反序列化演示
@Test
public void test(){

    //创建对象
    Person person = new Person();
    person.setName("void");
    person.setAge(26);
    person.setRemark("hello world");

    //指定 目标位置
    String target = "F:\\out\\s.txt";

    //序列化 演示
    try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(Files.newOutputStream(Paths.get(target)))) {

        objectOutputStream.writeObject(person);

    } catch (IOException e) {
        e.printStackTrace();
    }

    //反序列化 演示
    try (ObjectInputStream objectInputStream = new ObjectInputStream(Files.newInputStream(Paths.get(target)))) {

        Person person1 = (Person) objectInputStream.readObject();
        log.info("person1:{}", person1);
        //person1:Person(name=void, age=26, remark=null) 注意这里的remark字段,有transient关键字修饰和没有是两个结果
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
    }
}

源码解析

前文说到

  • Serializable起标识作用,标识该类可被序列化,如果不实现该接口,调用序列化方法会报java.io.NotSerializableException
  • transient关键字标记的字段不会被序列化
    从源码来验证:

Serializable起标识作用原理
java.io.ObjectOutputStream#writeObject0()方法中的代码片段
可以看到,如果这个类既不是字符串,数组,枚举类,也没有实现Serializable接口,就会报(NotSerializableException)错

private void writeObject0(Object obj, boolean unshared)
        throws IOException
{
        ...
        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());
           }
        }
        ...
}
//...

transient关键字标记的字段不会被序列化原理
java.io.ObjectStreamClass.getDefaultSerialFields中的代码片段
这里涉及一种关键的数学和计算机科学知识点,即通过位运算,一个整数能够被精确无误地分解为多个具有唯一确定性的二进制子串。换言之,对于任何整数,我们都可以利用位运算技术将其分割成多个独一无二、确定无疑的二进制表示状态

private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
        Field[] clFields = cl.getDeclaredFields();
        ArrayList<ObjectStreamField> list = new ArrayList<>();
        //注意点1: Modifier 是 Java中用来表示修饰符的一个类 一个整数可以通过位运算聚合多种状态
        int mask = Modifier.STATIC | Modifier.TRANSIENT;

        for (int i = 0; i < clFields.length; i++) {
            //注意点2: 通过位运算与(都是1才是1),判断如果该字段 既不是static修饰也不是transient修饰的字段 就需要序列化
            if ((clFields[i].getModifiers() & mask) == 0) {
                list.add(new ObjectStreamField(clFields[i], false, true));
            }
        }
        int size = list.size();
        return (size == 0) ? NO_FIELDS :
            list.toArray(new ObjectStreamField[size]);
    }

怎么自定义序列化和反序列化方法?

参考ArrayList源码

//ArrayList中的自定义序列化方法
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    int expectedModCount = modCount;
    //注意点1:调用 ObjectOutputStream的默认 序列化方法将该序列化的字段序列化
    s.defaultWriteObject();

    //注意点2:额外写入数组的实际装了多少元素(不是总容量)
    //Write out size as capacity for behavioural compatibility with clone()    
    s.writeInt(size);

    //注意点3:依次写入数组元素
    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;

    //注意点4:调用ObjectInputStream的默认 反序列化方法将该反序列化的字段反序列化
    s.defaultReadObject();

    //注意点5:这里读取的值是被忽略的
    // Read in capacity
    s.readInt(); // ignored
    
    //注意点6: 依次反序列化    
    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        int capacity = calculateCapacity(elementData, size);
        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, 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();
        }
    }
}

参考源码注释和补充的批注能大概理解整个流程,但是这里有个地方比较让我疑惑
结合注意点2,和注意点5发现ArrayList在自定义序列化方法额外写入了size
但是反序列化时仅仅只做了读取并没有使用,源码注释也是//ignore,序列化写入的时候也提了一下写入size是为了兼容clone()行为
参考文章https://www.zhihu.com/question/359634731 应该是版本兼容问题

新的问题?为什么写了writeObject()方法和readObject()方法,序列化和反序列化就会按照自定义的来?

序列化反序列化自定义原理

还是结合源码分析文章来源地址https://www.toymoban.com/news/detail-840159.html

//1.以下为java.io.ObjectOutputStream#writeSerialData()的源码
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;
        //注意点1:这里进行了是否有WriteObject方法的判定
        if (slotDesc.hasWriteObjectMethod()) {
            PutFieldImpl oldPut = curPut;
            curPut = null;
            SerialCallbackContext oldContext = curContext;

            if (extendedDebugInfo) {
                debugInfoStack.push(
                    "custom writeObject data (class \"" +
                    slotDesc.getName() + "\")");
            }
            try {
                curContext = new SerialCallbackContext(obj, slotDesc);
                bout.setBlockDataMode(true);
                slotDesc.invokeWriteObject(obj, this);
                bout.setBlockDataMode(false);
                bout.writeByte(TC_ENDBLOCKDATA);
            } finally {
                curContext.setUsed();
                curContext = oldContext;
                if (extendedDebugInfo) {
                    debugInfoStack.pop();
                }
            }

            curPut = oldPut;
        } else {
            defaultWriteFields(obj, slotDesc);
        }
    }
}
//2.进入方法 slotDesc.hasWriteObjectMethod()
boolean hasWriteObjectMethod() {
    requireInitialized();
    //注意点2:这里对成员变量writeObjectMethod 进行了判断 以此为依据来确定类是否含有writeObject方法 什么时候赋值的?(初始化)
    return (writeObjectMethod != null);
}
//3.在java.io.ObjectStreamClass.ObjectStreamClass(java.lang.Class<?>)类构造方法中 进行了初始化
private ObjectStreamClass(final Class<?> cl){
    ...    
    if(externalizable){
        cons=getExternalizableConstructor(cl);
    }else{
        cons=getSerializableConstructor(cl);
        //注意点3:这里使用了反射机制为成员变量writeObjectMethod是否含有方法writeObject方法进行了赋值判定
        writeObjectMethod=getPrivateMethod(cl,"writeObject",
            new Class<?>[]{ObjectOutputStream.class },
            Void.TYPE);
        readObjectMethod=getPrivateMethod(cl,"readObject",
            new Class<?>[]{ObjectInputStream.class },
            Void.TYPE);
        readObjectNoDataMethod=getPrivateMethod(
            cl,"readObjectNoData",null,Void.TYPE);
        hasWriteObjectData=(writeObjectMethod!=null);
    }
    ...
}

到了这里,关于Java序列化和反序列化机制的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 一文了解Java序列化和反序列化:对象的存储与传输

    Java序列化是一项强大而重要的技术,它允许我们将对象转换为字节流,以便在存储、传输和重建时使用。在本文中,我们将深入探讨Java序列化的基本概念、使用方法以及一些应用场景。 Java序列化是指将对象转换为字节流的过程,以便可以将其存储到文件、数据库或在网络上

    2024年02月13日
    浏览(34)
  • 想要精通算法和SQL的成长之路 - 二叉树的序列化和反序列化问题

    想要精通算法和SQL的成长之路 - 系列导航 二叉树的层序遍历 像这种从上至下并且按层打印的,可以称之为 二叉树的广度优先搜索( BFS ) 。而这类算法往往借助 队列的一个先入先出特性 来实现。 那么有这么几个步骤: 1.特殊处理还有初始化动作。 2. BFS 循环: 最终完整代

    2024年02月07日
    浏览(46)
  • 又一个难题:Java 序列化和反序列化为什么要实现 Serializable 接口?

    作者:椰子Tyshawn 来源:https://blog.csdn.net/litianxiang_kaola 最近公司的在做服务化, 需要把所有model包里的类都实现Serializable接口, 同时还要显示指定serialVersionUID的值. 听到这个需求, 我脑海里就突然出现了好几个问题, 比如说: 序列化和反序列化是什么? 实现序列化和反序列化为什

    2024年02月08日
    浏览(50)
  • 使用序列化和反序列化函数archivedDataWithRootObject和unarchivedObjectOfClasses的使用和遇到问题及解决方案

    为何archiveRootObject和unarchiveObjectWithFile正常,而archivedDataWithRootObject和unarchivedObjectOfClasses一直报错。 [NSKeyedArchiver archiveRootObject:account toFile:path]; 和`c PPAccountModel *account = [NSKeyedUnarchiver unarchiveObjectWithFile:path]; 替换很简单,但是会一堆问题等着你解决,序列化和反序列全失败。

    2024年02月16日
    浏览(35)
  • 【Linux】序列化和反序列化

    在网络编程中,直接使用 结构体 进行数据传输会出错,因为 本质上socket无法传输结构体 ,我们只有将结构体装换为字节数组,或者是字符串格式来传输,然后对端主机收到了数据,再将其转化为结构体,这就是序列化和反序列化的过程! 序列化 (Serialization)是将对象的状态

    2024年02月10日
    浏览(40)
  • Unity-序列化和反序列化

    序列化是指把对象转换为字节序列的过程,而反序列化是指把字节序列恢复为对象的过程。序列化最主要的用途就是传递对象和保存对象。 在Unity中保存和加载、prefab、scene、Inspector窗口、实例化预制体等都使用了序列化与反序列化。 1 自定义的具有Serializable特性的非抽象、

    2024年01月24日
    浏览(54)
  • 什么是序列化和反序列化?

    JSON(JavaScript Object Notation)和XML(eXtensible Markup Language)是两种常用的数据交换格式,用于在不同系统之间传输和存储数据。 JSON是一种轻量级的数据交换格式,它使用易于理解的键值对的形式表示数据。JSON数据结构简单明了,易于读写和解析,是基于JavaScript的一种常用数据

    2024年02月09日
    浏览(54)
  • jackjson自定义序列化和反序列化

    JRT引用的jackjson作为json处理库。由于JRT.ORM要求表不用datetime类型,把日期和时间用Int存储,所以ORM要支持日期时间的转换。为什么要把日期时间不用datetime而用Int,比如日期:20240117,时间就是从0点到当前的秒数。因为不用datetime兼容性好,不会因为不同库datetime函数不同而要

    2024年01月18日
    浏览(36)
  • [计算机网络]---序列化和反序列化

    前言 作者 :小蜗牛向前冲 名言 :我可以接受失败,但我不能接受放弃    如果觉的博主的文章还不错的话,还请 点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正  目录  一、再谈协议 二、序列化和反序化 1、网络版本计算器的场景搭建 2、

    2024年02月20日
    浏览(42)
  • TCP定制协议,序列化和反序列化

    目录 前言 1.理解协议 2.网络版本计算器 2.1设计思路 2.2接口设计 2.3代码实现: 2.4编译测试 总结         在之前的文章中,我们说TCP是面向字节流的,但是可能对于面向字节流这个概念,其实并不理解的,今天我们要介绍的是如何理解TCP是面向字节流的,通过编码的方式,自

    2024年02月12日
    浏览(33)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包