首页 > 编程语言 >Java序列化和反序列化机制

Java序列化和反序列化机制

时间:2024-03-15 10:00:51浏览次数:20  
标签:Java transient obj new 机制 序列化 size

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()方法,序列化和反序列化就会按照自定义的来?

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

还是结合源码分析

//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,transient,obj,new,机制,序列化,size
From: https://www.cnblogs.com/void-cmy/p/18074769

相关文章

  • 猫头虎分享已解决Bug | 成功解决java.lang.OutOfMemoryError: Java heap space错误
    博主猫头虎的技术世界......
  • [转][Java] Date 的替代品 Instant
    来自:https://mp.weixin.qq.com/s/SotzqrKAfrND88n12QFCEA怎么改?只能说这种基础的类改起来牵一发动全身,需要从DO实体类看起,然后就是各种Converter,最后是DTO。由于我们还是微服务架构,业务服务依赖于基础服务的API,所以必须要一起改否则就会报错。这里就不细说修改流程了,主要说一下......
  • JavaScript字符串和时间处理随笔
    2024-3-15记事1//待处理数组2letarr=[];3//筛选数组某个字段(某一列)4letjieshus=arr.railways.map(item=>item.jieshu);5//获取当前时间时间戳6letnow=Date.now();7//获取当前时间并转化成指定格式的日期字符串8letdate=newDate().toLo......
  • Javaweb项目使用本地servlet启动,可以弹出主页,跳转到controller报404解决方案
    首先检查项目的资源路径,以及tomcat配置,有没有部署,上下文配置好如果问题依然出现,那么可以考虑tomcat版本与依赖不匹配,我用的是tomcat10,使用使用这个依赖,就解决了这个问题,jakarta.servletjakarta.servlet-api5.0.0provided,相应的匹配版本可以查询到。......
  • JAVA中文乱码浅析及解决方案
            在Java编程中,中文乱码问题是程序员经常面临的一个挑战。中文乱码指的是在处理中文字符时,由于字符编码不统一或者编码转换错误导致的字符显示不正常、无法正确解析的问题。本文将从中文乱码的原因分析开始,然后介绍一些常见的解决方案,帮助程序员有效地解决这一问......
  • Java基于 Springboot+Vue 的招生管理系统,前后端分离
    博主介绍:✌程序员徐师兄、8年大厂程序员经历。全网粉丝15w+、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌......
  • JAVA学习记录01
    String为什么是不可变的?保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。如何创建线程?一般来说,创建线程有很多种方式,例如继承Thread类、实现......
  • Java之路之第1天
    大家好,作为一个在java自学之路上断档无数次fw,这次下定决心想要把java学好。故此也在这里开通了博客,记录自己的成长之路。今天在B站听了“狂神说java”的课,感觉又有了学习的动力。废话不多说,展示一下今天的成果。今天学习了如何写博客和做笔记,这里推荐笔记工具——Typora,学习的语......
  • 【Java面试题-基础知识02】Java抽象类和接口六连问?
    1、抽象类和接口分别是什么?抽象类是一种类,可以包含抽象方法和非抽象方法,抽象方法是没有具体实现的方法,需要在子类中被具体实现。接口是一种完全抽象的类,其中的所有方法都是抽象方法,没有方法体,它只是定义了一组方法的契约。2、接口中一定不可以有实现方法吗?不一定,Java8引入......
  • Java每日练习——2
    题目一:下列说法正确的是A:在类方法中可用this来调用本类的类方法B:在类方法中调用本类的类方法可直接调用C:在类方法中只能调用本类的类方法D:在类方法中绝对不能调用实例方法题目二:有如下代码:请写出程序的输出结果。publicclassTest{publicstaticvoidmain(String[]......