深度理解 JAVA序列化

news2024/12/27 1:37:23

在这里插入图片描述
前言
相信大家日常开发中,经常看到Java对象“implements Serializable”。那么,它到底有什么用呢?本文从以下几个角度来解析序列这一块知识点~

  • 什么是Java序列化?
  • 为什么需要序列化?
  • 序列化用途
  • Java序列化常用API
  • 序列化的使用
  • 序列化底层
  • 日常开发序列化的注意点
  • 序列化常见面试题

一、什么是Java序列化?🍉

序列化:把Java对象转换为字节序列的过程

反序列化:把字节序列恢复为Java对象的过程

二、为什么需要序列化?🍉

Java对象是运行在JVM的堆内存中的,如果JVM停止后,它的生命也就戛然而止。

在这里插入图片描述
如果想在JVM停止后,把这些对象保存到磁盘或者通过网络传输到另一远程机器,怎么办呢?磁盘这些硬件可不认识Java对象,它们只认识二进制这些机器语言,所以我们就要把这些对象转化为字节数组,这个过程就是序列化啦~

打个比喻,作为大城市漂泊的码农,搬家是常态。当我们搬书桌时,桌子太大了就通不过比较小的门,因此我们需要把它拆开再搬过去,这个拆桌子的过程就是序列化。 而我们把书桌复原回来(安装)的过程就是反序列化啦。

三、序列化用途🍉

序列化使得对象可以脱离程序运行而独立存在,它主要有两种用途:

1) 序列化机制可以让对象地保存到硬盘上,减轻内存压力的同时,也起了持久化的作用

比如 Web服务器中的Session对象,当有 10+万用户并发访问的,就有可能出现10万个Session对象,内存可能消化不良,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。

2) 序列化机制让Java对象在网络传输不再是天方夜谭。

我们在使用Dubbo远程调用服务框架时,需要把传输的Java对象实现Serializable接口,即让Java对象序列化,因为这样才能让对象在网络上传输。

四、Java序列化常用API🍉

java.io.ObjectOutputStream
java.io.ObjectInputStream
java.io.Serializable
java.io.Externalizable

Serializable 接口🥝

Serializable接口是一个标记接口,没有方法或字段。一旦实现了此接口,就标志该类的对象就是可序列化的。

public interface Serializable {
}

Externalizable 接口🥝

Externalizable继承了Serializable接口,还定义了两个抽象方法:writeExternal()和readExternal(),如果开发人员使用Externalizable来实现序列化和反序列化,需要重写writeExternal()和readExternal()方法

public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

java.io.ObjectOutputStream类🥝

表示对象输出流,它的writeObject(Object obj)方法可以对指定obj对象参数进行序列化,再把得到的字节序列写到一个目标输出流中。

java.io.ObjectInputStream🥝

表示对象输入流,它的readObject()方法,从输入流中读取到字节序列,反序列化成为一个对象,最后将其返回。

五、序列化的使用🍉

序列化如何使用?来看一下,序列化的使用的几个关键点吧:

  • 声明一个实体类,实现Serializable接口
  • 使用ObjectOutputStream类的writeObject方法,实现序列化
  • 使用ObjectInputStream类的readObject方法,实现反序列化

声明一个Student类,实现Serializable🥝

public class Student implements Serializable {
    private Integer age;
    private String name;
    public Integer getAge() {
        return age;
    }    public void setAge(Integer age) {
        this.age = age;
    }    public String getName() {
        return name;
    }    public void setName(String name) {
        this.name = name;
    }}

使用ObjectOutputStream类的writeObject方法,对Student对象实现序列化🥝

把Student对象设置值后,写入一个文件,即序列化

ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("D:\\text.out"));
Student student = new Student();
student.setAge(25);
student.setName("jayWei");
objectOutputStream.writeObject(student);objectOutputStream.flush();objectOutputStream.close();

使用ObjectInputStream类的readObject方法,实现反序列化,重新生成student对象🥝

ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\text.out"));
Student student = (Student) objectInputStream.readObject();System.out.println("name="+student.getName());

六、序列化底层🍉

Serializable底层🥝

Serializable接口,只是一个空的接口,没有方法或字段,为什么这么神奇,实现了它就可以让对象序列化了?

public interface Serializable {
}

为了验证Serializable的作用,把以上demo的Student对象,去掉实现Serializable接口,看序列化过程怎样吧~

在这里插入图片描述

序列化过程中抛出异常啦,堆栈信息如下:

Exception in thread "main" java.io.NotSerializableException: com.example.demo.Student
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
	at com.example.demo.Test.main(Test.java:13)

顺着堆栈信息看一下,原来有重大发现,如下~在这里插入图片描述

原来底层是这样:ObjectOutputStream 在序列化的时候,会判断被序列化的Object是哪一种类型,String?array?enum?还是 Serializable,如果都不是的话,抛出 NotSerializableException异常。所以呀,Serializable真的只是一个标志,一个序列化标志~

writeObject(Object)🥝

序列化的方法就是writeObject,基于以上的demo,我们来分析一波它的核心方法调用链吧~(建议大家也去debug看一下这个方法,感兴趣的话)

在这里插入图片描述
writeObject直接调用的就是writeObject0()方法

public final void writeObject(Object obj) throws IOException {
    ......    writeObject0(obj, false);
    ......}

writeObject0 主要实现是对象的不同类型,调用不同的方法写入序列化数据,这里面如果对象实现了Serializable接口,就调用writeOrdinaryObject()方法~

private void writeObject0(Object obj, boolean unshared)
        throws IOException    {    ......   //String类型
    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);   //Serializable实现序列化接口
    } 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());
        }    }    ......

writeOrdinaryObject()会先调用writeClassDesc(desc),写入该类的生成信息,然后调用writeSerialData方法,写入序列化数据

private void writeOrdinaryObject(Object obj,
                                     ObjectStreamClass desc,
                                     boolean unshared)
        throws IOException
    {
            ......            //调用ObjectStreamClass的写入方法
            writeClassDesc(desc, false);
            // 判断是否实现了Externalizable接口
            if (desc.isExternalizable() && !desc.isProxy()) {
                writeExternalData((Externalizable) obj);
            } else {
                //写入序列化数据
                writeSerialData(obj, desc);
            }
            .....
    }

writeSerialData()实现的就是写入被序列化对象的字段数据

private void writeSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        for (int i = 0; i < slots.length; i++) {
            if (slotDesc.hasWriteObjectMethod()) {
                   //如果被序列化的对象自定义实现了writeObject()方法,则执行这个代码块
                    slotDesc.invokeWriteObject(obj, this);
            } else {
                // 调用默认的方法写入实例数据
                defaultWriteFields(obj, slotDesc);
            }
        }
    }

defaultWriteFields()方法,获取类的基本数据类型数据,直接写入底层字节容器;获取类的obj类型数据,循环递归调用writeObject0()方法,写入数据~

private void defaultWriteFields(Object obj, ObjectStreamClass desc)
        throws IOException    {           // 获取类的基本数据类型数据,保存到primVals字节数组
        desc.getPrimFieldValues(obj, primVals);        //primVals的基本类型数据写到底层字节容器
        bout.write(primVals, 0, primDataSize, false);
        // 获取对应类的所有字段对象
        ObjectStreamField[] fields = desc.getFields(false);
        Object[] objVals = new Object[desc.getNumObjFields()];        int numPrimFields = fields.length - objVals.length;        // 获取类的obj类型数据,保存到objVals字节数组
        desc.getObjFieldValues(obj, objVals);        //对所有Object类型的字段,循环
        for (int i = 0; i < objVals.length; i++) {
            ......              //递归调用writeObject0()方法,写入对应的数据
            writeObject0(objVals[i],
                             fields[numPrimFields + i].isUnshared());
            ......
        }
    }

七、日常开发序列化的一些注意点🍉

  • static静态变量和transient 修饰的字段是不会被序列化的
  • serialVersionUID问题
  • 如果某个序列化类的成员变量是对象类型,则该对象类型的类必须实现序列化
  • 子类实现了序列化,父类没有实现序列化,父类中的字段丢失问题

static静态变量和transient 修饰的字段是不会被序列化的🥝

static静态变量和transient 修饰的字段是不会被序列化的,我们来看例子分析一波~ Student类加了一个类变量gender和一个transient修饰的字段specialty

public class Student implements Serializable {
    private Integer age;
    private String name;
    public static String gender = "男";
    transient  String specialty = "计算机专业";
    public String getSpecialty() {
        return specialty;
    }    public void setSpecialty(String specialty) {
        this.specialty = specialty;
    }    @Override
    public String toString() {
        return "Student{" +"age=" + age + ", name='" + name + '\'' + ", gender='" + gender + '\'' + ", specialty='" + specialty + '\'' +
                '}';
    }    ......

打印学生对象,序列化到文件,接着修改静态变量的值,再反序列化,输出反序列化后的对象~

运行结果:

序列化前Student{age=25, name='jayWei', gender='男', specialty='计算机专业'}
序列化后Student{age=25, name='jayWei', gender='女', specialty='null'}

对比结果可以发现:

  • 1)序列化前的静态变量性别明明是‘男’,序列化后再在程序中修改,反序列化后却变成‘女’了,what?显然这个静态属性并没有进行序列化。其实,静态(static)成员变量是属于类级别的,而序列化是针对对象的~所以不能序列化哦。

  • 2)经过序列化和反序列化过程后,specialty字段变量值由’计算机专业’变为空了,为什么呢?其实是因为transient关键字,它可以阻止修饰的字段被序列化到文件中,在被反序列化后,transient 字段的值被设为初始值,比如int型的值会被设置为 0,对象型初始值会被设置为null。

serialVersionUID问题🥝

serialVersionUID 表面意思就是序列化版本号ID,其实每一个实现Serializable接口的类,都有一个表示序列化版本标识符的静态变量,或者默认等于1L,或者等于对象的哈希码。

private static final long serialVersionUID = -6384871967268653799L;

serialVersionUID有什么用?🥝

JAVA序列化的机制是通过判断类的serialVersionUID来验证版本是否一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID和本地相应实体类的serialVersionUID进行比较,如果相同,反序列化成功,如果不相同,就抛出InvalidClassException异常。

接下来,我们来验证一下吧,修改一下Student类,再反序列化操作

在这里插入图片描述

Exception in thread "main" java.io.InvalidClassException: com.example.demo.Student;
local class incompatible: stream classdesc serialVersionUID = 3096644667492403394,
local class serialVersionUID = 4429793331949928814
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1876)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1745)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2033)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1567)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:427)
	at com.example.demo.Test.main(Test.java:20)

从日志堆栈异常信息可以看到,文件流中的class和当前类路径中的class不同了,它们的serialVersionUID不相同,所以反序列化抛出InvalidClassException异常。那么,如果确实需要修改Student类,又想反序列化成功,怎么办呢?可以手动指定serialVersionUID的值,一般可以设置为1L或者,或者让我们的编辑器IDE生成

private static final long serialVersionUID = -6564022808907262054L;

实际上,阿里开发手册,强制要求序列化类新增属性时,不能修改serialVersionUID字段~
在这里插入图片描述

如果某个序列化类的成员变量是对象类型,则该对象类型的类必须实现序列化🥝

给Student类添加一个Teacher类型的成员变量,其中Teacher是没有实现序列化接口的

public class Student implements Serializable {
        private Integer age;
    private String name;
    private Teacher teacher;
    ...}//Teacher 没有实现
public class Teacher  {
......
}

序列化运行,就报NotSerializableException异常啦

Exception in thread "main" java.io.NotSerializableException: com.example.demo.Teacher
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
	at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
	at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
	at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
	at com.example.demo.Test.main(Test.java:16)

其实这个可以在上小节的底层源码分析找到答案,一个对象序列化过程,会循环调用它的Object类型字段,递归调用序列化的,也就是说,序列化Student类的时候,会对Teacher类进行序列化,但是对Teacher没有实现序列化接口,因此抛出NotSerializableException异常。所以如果某个实例化类的成员变量是对象类型,则该对象类型的类必须实现序列化

在这里插入图片描述

子类实现了Serializable,父类没有实现Serializable接口的话,父类不会被序列化。🥝

子类Student实现了Serializable接口,父类User没有实现Serializable接口

//父类实现了Serializable接口
public class Student  extends User implements Serializable {    private Integer age;    private String name;}//父类没有实现Serializable接口public class User {    String userId;}Student student = new Student();student.setAge(25);
student.setName("jayWei");
student.setUserId("1");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\\text.out"));
objectOutputStream.writeObject(student);objectOutputStream.flush();
objectOutputStream.close();
//反序列化结果ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\text.out"));
Student student1 = (Student) objectInputStream.readObject();System.out.println(student1.getUserId());//output
/**  * null */

从反序列化结果,可以发现,父类属性值丢失了。因此子类实现了Serializable接口,父类没有实现Serializable接口的话,父类不会被序列化。

八、序列化常见面试题🍉

  • 序列化的底层是怎么实现的?
  • 序列化时,如何让某些成员不要序列化?
  • 在 Java 中,Serializable 和 Externalizable 有什么区别
  • serialVersionUID有什么用?
  • 是否可以自定义序列化过程, 或者是否可以覆盖 Java 中的默认序列化过程?
  • 在 Java 序列化期间,哪些变量未序列化?

1.序列化的底层是怎么实现的?🥝

本文第六小节可以回答这个问题,如回答Serializable关键字作用,序列化标志啦,源码中,它的作用啦还有,可以回答writeObject几个核心方法,如直接写入基本类型,获取obj类型数据,循环递归写入,哈哈

2.序列化时,如何让某些成员不要序列化?🥝

可以用transient关键字修饰,它可以阻止修饰的字段被序列化到文件中,在被反序列化后,transient 字段的值被设为初始值,比如int型的值会被设置为 0,对象型初始值会被设置为null。

3.在 Java 中,Serializable 和 Externalizable 有什么区别🥝

Externalizable继承了Serializable,给我们提供 writeExternal() 和 readExternal() 方法, 让我们可以控制 Java的序列化机制, 不依赖于Java的默认序列化。正确实现 Externalizable 接口可以显著提高应用程序的性能。

4.serialVersionUID有什么用?🥝

可以看回本文第七小节哈,JAVA序列化的机制是通过判断类的serialVersionUID来验证版本是否一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID和本地相应实体类的serialVersionUID进行比较,如果相同,反序列化成功,如果不相同,就抛出InvalidClassException异常。

5.是否可以自定义序列化过程, 或者是否可以覆盖 Java 中的默认序列化过程?🥝

可以的。我们都知道,对于序列化一个对象需调用 ObjectOutputStream.writeObject(saveThisObject), 并用 ObjectInputStream.readObject() 读取对象, 但 Java 虚拟机为你提供的还有一件事, 是定义这两个方法。如果在类中定义这两种方法, 则 JVM 将调用这两种方法, 而不是应用默认序列化机制。同时,可以声明这些方法为私有方法,以避免被继承、重写或重载。

6.在 Java 序列化期间,哪些变量未序列化?🥝

static静态变量和transient 修饰的字段是不会被序列化的。静态(static)成员变量是属于类级别的,而序列化是针对对象的。transient关键字修字段饰,可以阻止该字段被序列化到文件中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/734054.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android 热修复一

一、什么是热修复&#xff1f; 在我们应用上线后出现bug需要及时修复时&#xff0c;不用再发新的安装包&#xff0c;只需要发布补丁包&#xff0c;在客户无感知下修复掉bug。 实现效果&#xff1a; Demo源码&#xff1a; https://gitee.com/sziitjim/hotfix 二、怎么进行热修…

极速上手k8s,Kubernetes 从入门到摸鱼系列-理论篇

1. 引言&#x1f44b; 大家好&#xff0c;我是比特桃&#xff01;随着微服务架构越来越流行&#xff0c;大规模的微服务容器编排成了一件具有挑战的事情。在这次容器化云原生的发展中&#xff0c;Docker 成了容器化的赢家&#xff0c;而 Kubernetes 则成为了容器编排的赢家。k…

「已解决」 模块““umi“” ““@umijs/max“” 没有导出的成员“useRequest” “request” 问题的所有方法汇总

背景 使用 Umi 搭建项目时候有的时候会出现这种错误&#xff0c;模块““umi”” ““umijs/max”” 没有导出的成员“useRequest” “request”。 解决 tsconfig.json "paths": {"/*": ["src/*"],"/*": ["./src/.umi/*"…

用JShaman本地部署版,加密2.7MB的Webpack生成的JS文件

JShaman是知名的JS代码保护平台。在线使用&#xff0c;一键混淆加密&#xff0c;无需注册、无需登录。可免费用&#xff0c;也有商业服务&#xff1b;有在线使用的SAAS平台网站&#xff0c;也有本地部署版。很方便、很强大&#xff0c;很专业。 今天&#xff0c;测试使用JSham…

unordered_map模拟实现|STL源码剖析系列|开散列

博主很久没有更新过STL源码剖析这个系列的文章了&#xff0c;主要是因为大部分STL常用的容器&#xff0c;博主都已经发过文章了&#xff0c;今天博主带着大家把哈希表也模拟实现一下。 前言 那么这里博主先安利一下一些干货满满的专栏啦&#xff01; 手撕数据结构https://blo…

点云最小外包矩形计算

1、原理介绍 一簇点云的最小外包矩形&#xff08;Minimum Bounding Rectangle&#xff0c;MBR&#xff09;&#xff0c;是指用一个矩形将该簇点云框起来&#xff0c;所有点云数据在矩形框内。如下图所示为一个矩形框刚好将点云数据全部包围。 下面给出一种基于最大重叠度的最小…

[工业互联-19]:如何在QT中增加SOEM主站

目录 第1章 基本步骤 第2章 详细步骤 2.1.QT安装 2.2.VS安装 2.3.Win10 Debuggers 2.4.QT配置 2.5. SOEM移植 &#xff08;&#xff11;&#xff09;lib库生成 &#xff08;2&#xff09;文件移植: 文件整理 第1章 基本步骤 要在QT中添加SOEM主站功能&#xff0c;您需…

用OpenCV创建一张灰度黑色图像并设置某一列为白色

这段代码首先创建了一个400行600列的单通道灰度图像。然后,它遍历图像中的每个像素。如果像素位于列索引为30的列中,则将该像素的值设置为255。在灰度图像中,0表示黑色,255表示白色。因此,这段代码将图像的第30列设置为白色。 在 OpenCV 中,cv::Mat 构造函数的调用 cv::…

【算法 -- LeetCode】(13)罗马数字转整数

1、题目 罗马数字包含以下七种字符&#xff1a; I&#xff0c; V&#xff0c; X&#xff0c; L&#xff0c;C&#xff0c;D 和 M。 字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M …

哈希表和字符串专题1—205. 同构字符串 1002. 查找共用字符 925. 长按键入 844.比较含退格的字符串 C++实现

文章目录 205. 同构字符串1002. 查找共用字符925. 长按键入844.比较含退格的字符串栈模拟双指针 205. 同构字符串 class Solution { public:bool isIsomorphic(string s, string t) {unordered_map<char, char> map1;unordered_map<char, char> map2;for(int i0, j…

AI绘画:StableDiffusion炼丹Lora攻略-实战萌宠图片生成

Lora攻略-实战萌宠图片生成 写在前面的话一&#xff1a;准备二、Lora作用1.AI模特2.炼衣服Lora3.改变画风/画面背景Lora模型究竟是什么&#xff1f; 三、如何炼制自己的Lora模型&#xff1f;四、炼丹前的准备&#xff08;**下载整合包**&#xff09;五、选择合适的大模型六、高…

管理类联考——逻辑——记忆篇——数字编码——公式

&#x1f3e0;个人主页&#xff1a;fo安方的博客✨ &#x1f482;个人简历&#xff1a;大家好&#xff0c;我是fo安方&#xff0c;考取过HCIE Cloud Computing、CCIE Security、CISP、RHCE、CCNP RS、PEST 3等证书。&#x1f433; &#x1f495;兴趣爱好&#xff1a;b站天天刷&…

MySQL练习题(2)

创建如下员工标表 插入数据 1-- 按员工编号升序排列不在10号部门工作的员工信息 2-- 查询姓名第二个字母不是A且薪水大于1000元的员工信息&#xff0c;按薪水降序排列 4-- 求每个部门的平均薪水 5-- 求每个部门的最高薪水 6-- 求每个部门…

Coverity 2021.9 for win Coverity 2022.6 for linux

Coverity是一款快速、准确且高度可扩展的静态分析 (SAST) 解决方案&#xff0c;可帮助开发和安全团队在软件开发生命周期 (SDLC) 的早期解决安全和质量缺陷&#xff0c;跟踪和管理整个应用组合的风险&#xff0c;并确保符合安全和编码标准。Coverity 是一款精确的综合静态分析与…

JVM04-优化JVM内存分配以及内存持续上升问题和CPU过高问题排查

1-JVM内存分配 1.1-JVM内存分配性能问题 JVM内存分配不合理最直接的表现就是频繁的GC&#xff0c;这会导致上下文切换等性能问题&#xff0c;从而降低系统的吞吐量、增加系统的响应时间。因此&#xff0c;如果你在线上环境或性能测试时&#xff0c;发现频繁的GC&#xff0c;且…

密码学入门——分组密码模式

文章目录 参考书一、简介二、ECB模式三、CBC模式四、CFB模式五、OFB模式六、CTR模式 参考书 图解密码技术&#xff0c;第三版 一、简介 分组密码工作模式指的是将明文分成固定长度的块&#xff0c;并对每个块应用相同的加密算法进行加密的过程。这些块的长度通常是64位或128…

leetcode 654. 最大二叉树

2023.7.9 又是一道递归构造二叉树的题&#xff0c;和昨天做的那道题从中序与后序遍历序列构造二叉树类似&#xff0c;5分钟AC了。 大致思路就是通过找到数组中的最大值&#xff0c;并将其作为根节点&#xff0c;然后递归地构建左子树和右子树&#xff0c;最终返回整个最大二叉树…

PMP项目管理-敏捷

敏捷知识体系&#xff1a; 传统项目特点 1> 一开始就对详细的需求进行很高的投入 2> 价值只有到项目结束的时候才能体现, 风险较高 3> 一开始就要编写很多的文档 4> 客户参与度不高, 澄清完需求之后基本不参与 5> 需要花大量的时间来汇报当前的项目状态 6> 无…

Freertos-mini智能音箱项目---IO扩展芯片PCA9557

项目上用到的ESP32S3芯片引脚太少&#xff0c;选择了PCA9557扩展IO&#xff0c;通过一路i2c可以扩展出8个IO。这款芯片没有中断输入&#xff0c;所以更适合做扩展输出引脚用&#xff0c;内部寄存器也比较少&#xff0c;只有4个&#xff0c;使用起来很容易。 输入寄存器 输出寄存…

线程本地变量交换框架-TransmitterableThreadLocal(阿里开源)

上文 &#xff1a;秒级达百万高并发框架-Disruptor TransmitterableThreadLocal介绍 TransmitterableThreadLocal简称TTL 是阿里巴巴开源的一个框架。TransmittableThreadLocal是对Java中的ThreadLocal进行了增强和扩展。它旨在解决在线程池或异步任务调用链中&#xff0c;Thre…