Java序列化原理与实战:从serialVersionUID到反序列化安全
序列化这块几乎是Java面试中的必考题也是实际开发里绕不开的基础能力。不管是Redis缓存存对象、RPC调用传参、MQ消息投递还是做深拷贝背后都离不开序列化。但很多同学对它的理解停留在实现Serializable接口这一步一旦被问到serialVersionUID的作用、transient的边界、反序列化漏洞的原理就容易发懵。这篇博文把Java序列化的核心知识点完整梳理一遍包含底层机制、常见坑位、安全问题和现代选型建议力求让你看完不仅能应付面试更能避开生产环境里的那些雷。1. 序列化到底解决了什么问题1.1 序列化与反序列化的本质序列化Serialization就是把内存中的对象状态转换成字节序列的过程反序列化Deserialization是它的逆操作把字节序列重新还原成内存对象。为什么要做这种转换因为内存里的对象只活在当前JVM进程中一旦进程退出、网络传输、落盘存储对象就消失了。字节流是一种与运行环境无关的通用载体可以写入文件、数据库也可以塞进网络报文传到另一台机器上。用一个生活化的类比序列化就像是把一件实木家具拆成一块块木板并贴上标签方便搬运反序列化就是收到木板后照着标签重新组装成原来的家具。关键在于贴标签这一步——标签里记录的类信息、字段名、字段类型决定了对面能不能原样组装回来。这也是Java序列化机制里最核心的设计不仅保存对象的数据值还要保存对象的类描述信息类名、字段名、字段类型、继承关系等否则反序列化时无从还原。需要特别强调一点对象序列化保存的是数据快照不包含类的具体实现逻辑方法体。方法代码在类加载时已经由JVM绑定反序列化的目标是重建对象实例再通过引用去调用当前JVM里已加载的类方法。所以两端必须存在相同或兼容的类定义反序列化过程才能真正跑通。1.2 典型应用场景和选型逻辑实际开发中序列化的应用场景集中在四类远程调用RPC比如Dubbo、gRPC的远程方法调用服务提供方把返回对象序列化成字节流通过网络传到消费方消费方再反序列化成对象。这里选型时会优先考虑性能因为一次调用往往有大量消息。缓存存储RedisRedis本身是KV存储value只能存字符串或字节数组。Java对象想放进Redis要么手动转JSON字符串要么走JdkSerializationRedisSerializer配置对象序列化。消息队列MQKafka、RocketMQ的消息体本质也是字节数组生产端将对象序列化投递消费端反序列化还原。持久化与深拷贝把对象状态写入磁盘文件或数据库的BLOB字段或者借助序列化实现深拷贝写出去再读回来得到一个全新的对象。值得注意的一点是Java原生序列化ObjectOutputStream在这四类场景里现在都不算首选。RPC框架普遍使用Hessian、ProtobufRedis更推荐JSON或者自定义二进制MQ大多依赖broker自身的序列化插件。原生序列化更多出现在内部简单的持久化需求、官方文档示例以及各种面试题里。这个被替代的趋势不是没原因的后面我会专门用一节展开讲原生的缺点。2. Java原生序列化机制拆解2.1 Serializable空接口的魔法先看最基本的用法import java.io.Serializable; public class User implements Serializable { private static final long serialVersionUID 1L; private String name; private int age; // getter/setter 省略 }序列化接口是空接口里面没有任何方法。它的作用纯粹是标记告诉JVM这个类的对象可以被ObjectOutputStream写入。JVM内部的ObjectStreamClass会扫描类结构自动生成对应的序列化协议数据。这跟Cloneable、RandomAccess是同一类设计——标记接口。有人问普通类不实现Serializable直接序列化会怎样会抛NotSerializableException。ObjectOutputStream写入前会检查对象所属类是否实现了Serializable没实现就抛异常。这里有个容易忽视的细节成员变量类型也必须可序列化。比如User里有一个Address属性Address没实现Serializable序列化User时就报错。除非给该字段加transient跳过。还有一个关于继承的坑如果父类实现了Serializable那么子类自动可序列化不需要再标记如果父类没有实现子类实现了Serializable则父类字段不参与序列化因为父类不支持序列化协议反序列化时会调用父类的无参构造器来初始化父类部分。这个点后面章节再展开说。实现Serializable之后真正做序列化的核心类是ObjectOutputStream和ObjectInputStream// 序列化写出去 User user new User(张三, 25); try (ObjectOutputStream oos new ObjectOutputStream(new FileOutputStream(user.bin))) { oos.writeObject(user); } // 反序列化读回来 try (ObjectInputStream ois new ObjectInputStream(new FileInputStream(user.bin))) { User restored (User) ois.readObject(); }流程上就这三步创建流、writeObject/readObject、关闭。底层做了什么才是面试重点。2.2 serialVersionUID为什么每个类都要加serialVersionUID是Java序列化的版本号。它在反序列化时用来校验——字节流里记录的serialVersionUID和当前JVM中类的serialVersionUID如果不一致直接抛InvalidClassException。如果你不写serialVersionUID编译器会根据类名、字段名、方法名、继承关系等自动生成一个。问题在于这个自动生成的ID对类结构非常敏感哪怕只是新增一个字段、改了一个方法签名生成的serialVersionUID就变了。结果就是旧数据序列化出来的文件在新版本类上反序列化直接失败。举个例子假设线上版本V1的User有name和age两个字段生成的UID是A升级版本V2新增了email字段自动生成的UID变成B。Redis里还躺着V1序列化的缓存V2应用启动后一读抛InvalidClassException缓存全面失效。这在生产环境中是很令人抓狂的事。所以规范做法是显式声明一个固定的serialVersionUIDprivate static final long serialVersionUID 1L;这样不管类结构怎么演进只要UID不变反序列化就能兼容。字段增删的处理规则是新增字段会被设为默认值对象为null、数字为0删除字段时旧数据里对应的值会被忽略不报错。利用这个机制就可以做到一定程度的版本兼容。serialVersionUID具体用1L还是随意一个long都没问题关键是稳定。推荐按版本调整如果做了不兼容的大改动可以改UID强制旧数据作废否则保持不动。2.3 transient和static的边界transient是Java提供的关键字修饰字段表示这个字段不参与序列化。典型场景是密码、token、临时缓存这些不想落盘或者无法落盘的数据。还有那些引用了Socket、Thread、JdbcConnection之类的字段底层资源没法序列化必须用transient隔离。static字段为什么不参与序列化因为static字段属于类不属于对象。序列化保存的是对象状态而static的值是Class层面的不属于任何单个实例。反序列化后static字段的值来自当前类的静态初始化不是来自字节流。这里有一个经常被误解的点transient的字段反序列化后是什么值是非默认的初始值比如对象类型为null、int为0、boolean为false。跟new出来的对象一样如果类里给transient字段指定了初始值比如private transient int count 10;反序列化时这个初始值的赋值逻辑不会执行因为不走构造器而是走特殊机制所以count会变成0。这是个小坑如果依赖字段初始值要在readObject里补上。2.4 自己控制协议Externalizable接口如果觉得原生的序列化格式冗余太多它会把类名、字段名、字段签名全写进去可以换Externalizable接口自己定义序列化内容。它继承了Serializable但要求实现两个方法public class Order implements Externalizable { private String orderId; private BigDecimal amount; Override public void writeExternal(ObjectOutput out) throws IOException { out.writeUTF(orderId); out.writeObject(amount); } Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { orderId in.readUTF(); amount (BigDecimal) in.readObject(); } }自己控制的优势是输出流只包含你要的数据体积更小、速度更快而且字段名不会出现在字节流里对敏感信息有一定隐藏作用。代价是必须保证writeExternal和readExternal的读写顺序完全一致多了或少了都会导致错位反序列化出来完全不是你想要的值。这跟手动解析二进制协议一样读写不匹配的bug非常难排查。在用Externalizable时还有个规范反序列化时不会像Serializable那样通过特殊机制绕过构造器而是必须调用public的无参构造器。所以无参构造器一定得保留否则反序列化直接运行时异常。3. 序列化的底层流程与自定义钩子3.1 ObjectOutputStream写入时做了什么ObjectOutputStream.writeObject的底层逻辑大致分几步检查对象类型判断参数是否为String、Integer等基本包装类型或数组如果是走对应的特殊处理否则要求实现Serializable。写类描述将类的全限定名、serialVersionUID、字段数量、字段名和类型签名写入流。这一部分是Java原生序列化体积大的主要原因——每个对象都可能附带完整元数据除非多次写入同一个类时二次写入只写引用编号handle。写字段值按字段顺序依次写入非transient、非static的值。对象引用管理用一个handle table记录已经写过的对象。同一个对象第二次写入时不再重复写数据而是只写一个引用句柄。这样序列化后的数据中对象之间的共享关系得到保留反序列化后结构还能保持一致。这里有一个有意思的应用利用handle table可以做循环引用对象的序列化。比如A引用BB引用A这种循环引用用JSON序列化会栈溢出但Java原生序列化通过引用机制天然支持。反序列化readObject的流程则反过来读类描述→判断本地是否有对应类→创建实例不调用构造器→读字段值填充。3.2 自定义writeObject/readObject钩子Serializable接口允许类内部定义两个特殊签名的方法JVM在序列化和反序列化时检测到就会调用private void writeObject(ObjectOutputStream oos) throws IOException { oos.defaultWriteObject(); // 先写入默认字段 // 再写入额外数据比如业务签名、加密后的值 oos.writeUTF(extra-info); } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); // 先读回默认字段 // 读取自定义额外数据 String extra ois.readUTF(); }典型用途有两个。第一是对敏感字段二次处理writeObject时把密码加密后再写入readObject时解密还原。第二是补充transient字段的状态比如transient字段依赖普通字段计算在readObject里重新计算赋值。面试里经常问如何让serialVersionUID不兼容也能反序列化之类的非常规问题答案就是用自定义readObject做兼容处理。比如老版本数据没有某个字段可以在readObject里补上默认值而不是依赖字段声明。3.3 序列化实现深拷贝的细节深拷贝的一个通用实现方式就是拿对象做一次序列化再反序列化。代码很简单SuppressWarnings(unchecked) public static T T deepCopy(T obj) { try { ByteArrayOutputStream bos new ByteArrayOutputStream(); try (ObjectOutputStream oos new ObjectOutputStream(bos)) { oos.writeObject(obj); } ByteArrayInputStream bis new ByteArrayInputStream(bos.toByteArray()); try (ObjectInputStream ois new ObjectInputStream(bis)) { return (T) ois.readObject(); } } catch (IOException | ClassNotFoundException e) { throw new RuntimeException(e); } }使用前提是对象图里的所有类都得实现Serializable否则直接NotSerializableException。另外经过序列化的深拷贝不会执行任何构造器也就是说拷贝出来的对象不经过构造逻辑比如构造器里做的校验、默认值初始化全都不会执行。这一点非常关键——用序列化做深拷贝时如果类里有复杂的构造器逻辑结果会和预期不一致。还有一个性能问题基于原生序列化的深拷贝性能较差因为要生成完整格式的字节流。数据量大时建议改用Kryo一类的工具做深拷贝性能能提升好几倍。4. 序列化的经典坑位与破解方案4.1 单例和枚举的序列化陷阱序列化机制不调用构造器而是通过底层创建一个空的实例再填充字段。这对单例模式是致命的反序列化会绕过私有构造器直接产生第二个实例单例被破坏。解决方案是readResolvepublic class Singleton implements Serializable { private static final Singleton INSTANCE new Singleton(); private Singleton() {} public static Singleton getInstance() { return INSTANCE; } private Object readResolve() { return INSTANCE; } }readResolve的机制是readObject完成字段填充后会检查是否有readResolve方法如果有就用它返回的对象替代刚创建的对象。这样反序列化结果还是原来那个单例。枚举类型则天然免疫Java规范规定枚举反序列化时不会走普通对象创建流程而是通过类名和枚举常量名查找已有的枚举实例。所以枚举是实现单例最安全的方式不仅能防反射调用私有构造器也能防序列化破坏。面试中问到枚举单例为什么好的时候序列化安全是一个重要得分点。4.2 父类序列化与反序列化的构造器问题之前提到父类未实现Serializable、子类实现的情况。这里展开说细节因为它非常容易踩坑序列化时子类可序列化父类字段默认不序列化。写出的字节流里没有父类字段的数据。反序列化时JVM会调用父类的一个无参构造器把父类部分初始化好再填充子类的字段。如果父类没有无参构造器反序列化直接抛InvalidClassException错误信息是no valid constructor。比如public class Base { private String baseName; public Base(String name) { this.baseName name; } // 没有无参构造器 } public class Child extends Base implements Serializable { private int age; public Child(String name, int age) { super(name); this.age age; } }对Child做序列化没问题反序列化时会因为Base没有无参构造器而失败。解决办法是给Base补一个无参构造器哪怕什么都不做或者在Base也实现Serializable这样父类字段会走自己的序列化机制不依赖无参构造器。在父类也实现Serializable的情况下反序列化同样不会调用父类构造器而是通过底层机制直接创建实例。所以不要指望构造器里的初始化逻辑在反序列化时执行如果有必须的初始化要放到readObject里。4.3 版本演进的兼容性策略线上数据、缓存、消息队列里的历史序列化数据不会因为应用升级就消失。所以类结构的演进必须讲究策略。核心原则新增字段有默认值业务上如果要求不可为空要在readObject里做校验或赋默认值。删除字段旧数据里的值会被忽略不会报错。字段类型改动最危险比如把int改成long通常会导致反序列化异常因为这属于不兼容变更。类名或包名改动可以看成完全不同的类旧数据无法反序列化到新类。这种情况只能做数据迁移或者自定义readObject兼容。字段名改动本质也是不兼容变更反序列化按字段名匹配匹配不到就用默认值不报错但数据丢失。有一个实用技巧在readObject中主动检查必要的业务字段private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); if (name null || name.isEmpty()) { throw new IOException(name字段无效数据完整性校验失败); } }从安全角度看这种校验实际是必要的。反序列化等于把外界输入的字节流变成对象如果字节流里某些字段被恶意构造业务上又没有校验很容易引起不可预期的行为。所以readObject里加防御性校验既是兼容性手段也是安全手段。5. 反序列化安全为什么它是高危漏洞源头5.1 反序列化攻击的原理Java反序列化漏洞这几年一直是安全圈子里的重灾区历史上fastjson的多个漏洞、原生ObjectInputStream的gadget链问题都引起过大规模线上事故。要理解为什么反序列化危险先要明白一个事实readObject是一个从输入到对象的构造过程输入字节流完全是外部可控的。攻击者精心构造一个字节流让readObject在正常完成字段填充的过程中连带触发类的一些特殊方法最终形成攻击链条。经典的攻击思路叫做gadget链。攻击者不需要直接调用危险方法而是找到一条由多个类串联起来的调用路径。比如某个类的readObject方法会调用HashMap的put方法put会调用hashCode方法某个类的hashCode又会调用别的类的方法……这条链的终点往往是一个能执行系统命令、加载远程类或者发起网络请求的方法。一个典型的例子是URLDNS链常用于探测目标是否存在反序列化点构造HashMapkey里放一个URL对象URL的hashCode触发DNS查询。攻击者通过DNSlog平台看有没有收到请求就能确定目标服务器执行了反序列化。这类链路无害但能进行探测进一步配合真正的危险gadget就可以远程命令执行。反序列化攻击能成立的核心条件有三个有能够触发危险调用的gadget类存在比如commons-collections里的某些类应用对不可信的输入数据执行了readObject没有做类白名单限制。5.2 防御措施和最佳实践防御的方向很明确断开不可信输入和反射式对象创建之间的连接。白名单是最有效的方案。JDK 9开始提供了ObjectInputFilter可以在反序列化时过滤允许的类ObjectInputFilter filter ObjectInputFilter.Config.createFilter( com.example.model.*;java.base/*;!* ); try (ObjectInputStream ois new ObjectInputStream(inputStream)) { ois.setObjectInputFilter(filter); Object obj ois.readObject(); }配置规则里分号分隔多个规则*表示全包!表示拒绝类名匹配后立即终止。意思是允许com.example.model包下的类允许java.base模块的类拒绝其他一切类。这个白名单能挡掉绝大多数gadget链因为攻击者的链上必然包含不在白名单里的类。除了白名单还有几个实操建议永远不要对客户端直接传来的字节流做原生反序列化。凡是外部输入优先用JSON等文本格式。升级依赖版本。很多gadget链经过修复后不再可用保持组件版本最新能大幅降低被利用概率。反序列化前后做完整性校验比如加一层MAC签名防止字节流被篡改。尽量用高层次的替代方案Jackson、Gson、Protobuf这类基于字段映射的格式不自动调用对象的readObject安全性显著更好。fastjson的autoType问题就是反面教材默认开启类型自动识别后JSON字符串里可以指定任意类名攻击者指定一个危险类fastjson就帮它构建出来。后来官方默认关闭了autoType并加了黑名单但历史上已经造成了大量RCE事件。所以选型时安全因素和性能因素要一起考虑。6. Java生态中序列化方案的选型对比6.1 原生序列化的短板与适用边界原生序列化从JDK 1.1就有了设计目标很明确同一JVM内、同一类定义下的数据存取。跨语言、跨版本都不是它的强项。具体缺点可以归纳为体积膨胀流中包含完整类描述信息对包含大量字段和嵌套对象的结果体积远超JSON和二进制方案。对Redis这类以内存为贵的场景放大效应明显。性能差反射驱动字段扫描和写入的开销大大数据量下吞吐不乐观。安全性弱ANY类可反序列化一旦出现gadget就是RCE钓鱼和蠕虫攻击都很难防。跨语言不可用只有Java能读异构系统没法对接。协议不稳定serialVersionUID的兼容规则虽然清晰但跨大版本时仍然容易出问题尤其类结构变动频繁的时候。所以原生序列化的适用边界被压缩到了不需要跨语言、不受信任边界约束、数据量小、追求简单直接的场景。比如快速把对象缓存到本地临时文件、给测试代码做对象快照这些场景用它没毛病。任何要进网络、进分布式系统、要面对不可信输入的场景都值得考虑换方案。6.2 JSON方案灵活但要注意类型问题JSON是目前最主流的跨语言数据交换格式对应到Java生态有三个常用工具Jackson功能全面Spring Boot默认内置通过ObjectMapper的writeValueAsString和readValue完成序列化和反序列化。GsonGoogle出品API简洁依赖小适合轻量级场景。fastjson性能优秀但安全问题多autoType的历史漏洞让它在很多大厂被列入禁用名单。新项目不太推荐引入。JSON方案的优势是人类可读、调试友好、跨语言通用、体积适中。弱点是类型信息容易丢。举一个具体场景一个ListUser经过JSON序列化再反序列化后如果直接用List.class接收得到的其实是ListLinkedHashMap取元素时不能直接强转User。Jackson有个enableDefaultTyping相当于autoType能解决类型问题但开启后同样引入反序列化安全问题。这是JSON方案的经典两难类型安全与安全默认不可兼得。避免这个问题的安全姿势是反序列化时显式传入泛型类型ListUser users objectMapper.readValue(json, new TypeReferenceListUser() {});这样能拿到泛型信息还原出来的List元素直接就是User。6.3 二进制高性能方案Protobuf、Thrift、Kryo追求极致体积和性能的时候二进制序列化方案是首选。**ProtobufGoogle Protocol Buffers**是当前业界标杆。它先定义.proto文件生成对应的Java类序列化后体积小、速度快、跨语言能力强。因为数据结构是预定义编号字段的紧凑格式不传输字段名体积远小于JSON和Java原生格式。代价是要多一步Schema管理和代码生成的工序工程上引入一点额外成本但收益在大型系统中非常可观。Thrift跟Protobuf类似是Apache体系的RPC框架自带序列化协议和传输层。Kryo是纯Java的序列化库不需要预定义Schema使用简单性能也好。但它的跨语言支持很弱官方定位就是Java生态而且序列化结果对类的结构变化更敏感两个版本的类字段对不上很容易出问题。用Kryo做深拷贝、做Redis缓存value也是常见玩法但要注意它不会写入完整类描述性能好的同时牺牲了容错性。选型的核心逻辑很简单需要跨语言、追求稳定和高性能选Protobuf纯Java内部使用、图方便选Kryo需要人类可读、面向外部API选JSON。6.4 Redis场景下的序列化配置要点Redis在Java后端里几乎是标配了它本身不关心value格式只存字节。Spring Data Redis里有两种模板StringRedisTemplatevalue存字符串一般配合JSON序列化最简单可控。RedisTemplate默认使用JdkSerializationRedisSerializer也就是Java原生序列化。用默认配置存对象后在可视化管理工具里看到的是一堆乱码二进制而且体积大、安全风险高。这就是网上大量教程让你重写RedisTemplate的序列化方式的原因。常见的优化配置是自定义一个Jackson序列化器Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); ObjectMapper om new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); Jackson2JsonRedisSerializerObject serializer new Jackson2JsonRedisSerializer(Object.class); template.setKeySerializer(RedisSerializer.string()); template.setValueSerializer(serializer); template.setHashKeySerializer(RedisSerializer.string()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; }这个配置里的activateDefaultTyping会把类型信息写入JSON保证反序列化时能还原成原对象类型。但要注意这也复现了JSON类型信息的安全隐患只建议在纯内网、数据可信的前提下使用。更稳妥的做法是避免泛型擦除问题在业务代码里用TypeReference手动指定类型或者干脆用String序列化加一层对象JSON的手动转换。7. 面试真题视角高频问题背后的考察逻辑整理面试题的时候会发现序列化这块的问题其实可以分成三层概念层、机制层、安全与工程层。概念层问什么是序列化机制层问serialVersionUID有什么用transient的作用安全与工程层问如何防止反序列化攻击Redis用什么序列化方案。面试官真正想考察的不是你背了多久八股文而是你有没有在真实项目里被这些问题坑过。比如问serialVersionUID不写会怎样对应的就是能不能理解JVM自动生成的机制有没有遇到过InvalidClassException。问HashMap为什么不能序列化或如何让一个类不可序列化考察的是对底层机制的理解。一个高质量的Serializable类应该包含显式serialVersionUID、transient隔离敏感或瞬态字段、自定义readObject做防御校验、必要时提供readResolve保护单例。public class SecureToken implements Serializable { private static final long serialVersionUID 1L; private transient String rawToken; private String encryptedToken; public SecureToken(String rawToken) { this.rawToken rawToken; // 实际项目中这里做加密 this.encryptedToken encrypted: rawToken; } private void writeObject(ObjectOutputStream oos) throws IOException { oos.defaultWriteObject(); // 不写入rawToken它已经是transient } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); if (encryptedToken null || encryptedToken.isEmpty()) { throw new InvalidObjectException(token数据不完整); } // 实际项目中这里做解密 this.rawToken encryptedToken.replaceFirst(^encrypted:, ); } private Object readResolve() { // 如果此类未来变单例这里可以保护 return this; } }这种教科书式的写法在真实项目里不多见但面试官看到你能把多个知识点串起来会非常加分。8. 实际项目里的经验与建议最后想分享几个实际项目里攒下来的经验。第一新项目能不用原生序列化就别用。我见过线上缓存因为类新增字段导致的InvalidClassException整体熔断也见过因为对象图里嵌套了不可序列化的第三方类导致上线后才发现问题的场景。原生序列化的隐性问题只有踩过坑才会重视。第二涉及外部输入的序列化先想安全再想功能。不仅仅是原生ObjectInputStream任何开启了类型自动识别的JSON工具都一样危险。公司内部有安全红线的话最好在评审阶段就把这些问题前置处理掉。第三serialVersionUID不是小事。养成所有实现Serializable的类都显式声明UID的习惯顺手写一个注释说明这个类变更的历史后面的人维护起来会轻松很多。分布式系统里兼容性问题往往要比性能问题更棘手因为出问题的时候往往是流量高峰根本没有给你优雅升级的时间。如果你正在准备面试建议把这篇涉及的面试题逐个过一遍什么是序列化、Serializable和Externalizable的区别、serialVersionUID的作用、transient和static的区别、反序列化为什么不走构造器、怎么用序列化做深拷贝、如何破坏单例、枚举为什么不怕序列化、反序列化漏洞的原理、Redis缓存对象用什么序列化方案、JSON工具的安全问题、Protobuf相比JSON的优势。每个问题能讲出原理还能附带一个项目里的真实例子这个方向的知识就算是扎实了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →