Java通用比较模块实战:对象差异、集合比较与多字段排序
发布时间:2026/8/31 3:23:12来源:尧图网络
排查线上问题时最常遇到的一种场景是上游系统传过来一个对象我们落库之后觉得“数据好像变了”但又说不出到底变了哪里。两个对象在不同环境里打印出来都是明文字段字段少的时候肉眼还能找字段一旦到了二三十个人眼比对基本靠猜。很多人的第一反应是用equals比较但equals只能回答“一样还是不一样”回答不了“哪里不一样”。真正需要的是一个能输出“字段名 旧值 新值”的比较模块。本文会从零实现一个通用比较模块不依赖 Spring、不依赖第三方 JSON 库纯 JDK 8 及以上就能跑通。它会覆盖三类高频场景对象字段级差异比较、集合交集差集并集比较、多字段排序比较。这三类场景在数据审计、配置对比、接口入参校验、报表排序中都经常出现。读完你可以直接把它内嵌到自己的工具类里也可以按照文中的设计思路重写成一版适合业务字段的比较器。1. 这篇文章真正要解决的问题先明确一下什么叫“比较模块”。它不是 JDK 里某个类的别名而是业务系统里负责“判定两个数据对象之间的关系并输出可读结果”的功能组件。这里的核心不是“比较”这个动作本身而是比较结果如何表达。线上排查问题时你往往不是想知道两个对象“是否相等”而是想知道“是谁变了、从什么值变成什么值、影响哪一个字段”。如果没有统一的比较模块这类需求通常会被写成一堆散落的equals、Objects.equals、if (a.getX() ! b.getX())判断逻辑重复还很容易漏字段。具体来说比较模块要解决的痛点有四类第一数据审计。订单、用户、配置等核心数据在更新前和后需要计算差异记录到日志或审计表中。这时候需要字段级差异结果。第二配置对比。不同环境、不同版本的配置内容需要进行一致性校验找到差异项。第三接口变更识别。接口返回的数据结构升级后希望快速知道哪些字段是新增、删除或变更的。第四排序和去重。集合里的元素需要按多个字段排序或者在两个集合中找出新增、删除、不变的数据。第四点看起来和差异比较不同但都属于“比较模块”的能力范围。因此这篇文章不是只写一个工具方法而是写一套可以放进项目工具包的设计方案。它包含三块内容对象字段差异比较、集合差异比较、多字段排序比较。如果你正在做 Java 后端开发或者正在排查数据不一致问题又或者只是想让自己的工具类更完整这篇文章都值得读下去。反过来如果你的需求只是判断两个字符串或两个 Integer 是否相等用Objects.equals就够了不需要引入一个模块。2. 基础概念与核心原理要理解比较模块先要区分三类容易混淆的比较相等性比较、顺序比较、差异比较。相等性比较回答“是否相同”典型实现是equals和hashCode。它一般用于集合去重、Map 查找、缓存 key 判断。顺序比较回答“谁大谁小”典型实现是Comparable和Comparator。它用于排序、求最大值最小值、构造优先队列。差异比较回答“具体差在哪里”典型实现是业务侧自写的对比方法它输出的是一个差异集合而不只是一个布尔值或一个整数。用表格对比会更清楚比较类型典型接口返回值使用场景相等性比较equals / hashCodeboolean / int集合去重、缓存 key、幂等判断顺序比较Comparable / Comparatorint负数/0/正数List 排序、TreeMap、PriorityQueue差异比较自定义 compare 方法ListDiffField数据审计、配置对比、变更识别很多初学者会把Comparable和Comparator混为一谈。简单理解Comparable是“类自己知道自己怎么排序”写在实体类内部Comparator是“外部提供一个排序策略”写在类外面。一个类只能有一个自然排序但可以有多个外部比较器。比如User默认按年龄排序但实际场景里有时要按创建时间排有时要按姓名排这时应该写多个Comparator而不是改Comparable。继续说差异比较。实现对象差异比较通常有三条技术路线各有利弊实现路线优点缺点反射通用比较零侵入、适配任意 POJO、代码量少性能一般、难处理嵌套结构、容易踩访问权限坑显式字段比较性能好、可控性强、业务含义清晰字段多时代码量大新增字段容易漏序列化快照比较调试方便、直观、可直接输出 JSON 差异依赖 JSON 库、性能差、无法处理循环引用从工程角度讲我的判断是生产环境优先用显式字段比较因为可靠工具类和通用模块可以用反射兜底因为省代码序列化快照比较适合线下排查不适合放在高频请求链路上。本文的重点是用反射实现一个通用版本然后告诉你哪些地方必须改成显式比较才能上生产。3. 环境准备与前置条件本文的示例代码基于 JDK 8 及以上版本因为会用到java.util.stream、java.lang.reflect等标准库能力。项目不需要引入任何第三方依赖不需要 Spring 容器不需要 Maven 插件。JDK 8 本身已经足够。为了让你本地验证起来更清爽建议按下面的目录结构组织代码compare-module └── src └── com └── example └── compare ├── DiffField.java ├── ObjectDiffUtil.java ├── CollectionCompareUtil.java ├── SortUtil.java ├── UserInfo.java └── Main.java如果你习惯用 IDE直接创建一个普通 Java 工程把上面几个类放进去即可。如果你更习惯命令行可以用下面的命令编译和运行javac -encoding UTF-8 src/com/example/compare/*.java -d out java -cp out com.example.compare.Main这里不会强调具体 IDE 版本因为本文重点是 Java 标准库能力在 IntelliJ IDEA、Eclipse、VS Code 里都能运行。需要提醒的是反射机制对类名、字段名敏感重构实体类时如果改了字段名对应的差异报文字段路径也会变化这是正常现象不算 bug。4. 核心流程拆解设计一个对象差异比较模块设计对象差异比较模块之前先明确一个需求边界compare(Object oldObj, Object newObj)方法返回一个差异字段列表每个差异字段至少包含字段路径、字段名、旧值、新值。这样才能支撑后续的日志打印、审计入库、前端展示。先设计差异结果类。它的字段可以这样定义// 文件路径src/com/example/compare/DiffField.java public class DiffField { /** 字段路径支持嵌套例如 user.address.city */ private final String path; /** 简单字段名 */ private final String fieldName; /** 变更前的值 */ private final Object oldValue; /** 变更后的值 */ private final Object newValue; public DiffField(String path, String fieldName, Object oldValue, Object newValue) { this.path path; this.fieldName fieldName; this.oldValue oldValue; this.newValue newValue; } public String getPath() { return path; } public String getFieldName() { return fieldName; } public Object getOldValue() { return oldValue; } public Object getNewValue() { return newValue; } Override public String toString() { return DiffField{ path path \ , fieldName fieldName \ , oldValue oldValue , newValue newValue }; } }有了结果类下一步就是核心比较逻辑。用反射比较对象时需要按顺序处理几个问题第一步获取两个对象的 Class。比较前先判断二者是否为同一类型如果类型不同直接返回“类型不一致”的差异或者直接抛异常。从容错角度看比较模块更适合返回差异结果而不是抛异常因为调用方可能需要对差异做后续处理。第二步获取类声明的字段。使用clazz.getDeclaredFields()获取所有字段并调用field.setAccessible(true)允许访问私有字段。JDK 9 以上的模块系统对私有字段反射有一些限制但普通 classpath 应用仍然可用。如果目标对象属于 Java 系统库类反射会有限制所以工具类通常只用于业务 POJO。第三步逐个字段比较值。对于基本类型、包装类型、String、BigDecimal、日期等“叶子字段”直接判断是否相等对于嵌套对象应该递归进入子对象路径拼接成parent.child的形式对于集合和 Map本文做简化处理优先比较 toString 结果或大小更精确的集合差异可以交给集合比较模块。第四步设置深度限制。如果不限制递归深度两个互相引用的对象会导致StackOverflowError。这个问题在反射比较里非常典型。下面给出一个可运行的ObjectDiffUtil核心实现// 文件路径src/com/example/compare/ObjectDiffUtil.java import java.lang.reflect.Field; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Objects; public class ObjectDiffUtil { private static final int MAX_DEPTH 5; public static ListDiffField compare(Object oldObj, Object newObj) { ListDiffField diffs new ArrayList(); compareInternal(, oldObj, newObj, diffs, 0); return diffs; } private static void compareInternal(String pathPrefix, Object oldObj, Object newObj, ListDiffField diffs, int depth) { if (depth MAX_DEPTH) { return; } if (oldObj null newObj null) { return; } if (oldObj null || newObj null) { diffs.add(new DiffField(pathPrefix.isEmpty() ? root : pathPrefix, pathPrefix.isEmpty() ? root : pathPrefix, oldObj, newObj)); return; } if (isSimpleType(oldObj) || isSimpleType(newObj)) { if (!Objects.equals(oldObj, newObj)) { diffs.add(new DiffField(pathPrefix.isEmpty() ? root : pathPrefix, pathPrefix.isEmpty() ? root : pathPrefix, oldObj, newObj)); } return; } if (oldObj instanceof Collection || newObj instanceof Collection) { compareCollection(pathPrefix, oldObj, newObj, diffs, depth); return; } if (oldObj instanceof Map || newObj instanceof Map) { compareMap(pathPrefix, oldObj, newObj, diffs, depth); return; } if (!oldObj.getClass().equals(newObj.getClass())) { diffs.add(new DiffField(pathPrefix.isEmpty() ? root : pathPrefix, type, oldObj.getClass().getName(), newObj.getClass().getName())); return; } Field[] fields oldObj.getClass().getDeclaredFields(); for (Field field : fields) { try { field.setAccessible(true); String fieldName field.getName(); String childPath pathPrefix.isEmpty() ? fieldName : pathPrefix . fieldName; Object oldValue field.get(oldObj); Object newValue field.get(newObj); compareInternal(childPath, oldValue, newValue, diffs, depth 1); } catch (IllegalAccessException e) { diffs.add(new DiffField(pathPrefix, field.getName(), read-error, e.getMessage())); } } } private static void compareCollection(String pathPrefix, Object oldObj, Object newObj, ListDiffField diffs, int depth) { String oldText oldObj null ? null : oldObj.toString(); String newText newObj null ? null : newObj.toString(); if (!oldText.equals(newText)) { diffs.add(new DiffField(pathPrefix, pathPrefix.isEmpty() ? collection : pathPrefix, oldText, newText)); } } private static void compareMap(String pathPrefix, Object oldObj, Object newObj, ListDiffField diffs, int depth) { String oldText oldObj null ? null : oldObj.toString(); String newText newObj null ? null : newObj.toString(); if (!oldText.equals(newText)) { diffs.add(new DiffField(pathPrefix, pathPrefix.isEmpty() ? map : pathPrefix, oldText, newText)); } } private static boolean isSimpleType(Object obj) { return obj instanceof CharSequence || obj instanceof Number || obj instanceof Boolean || obj instanceof Character || obj instanceof Date || obj instanceof BigDecimal || obj.getClass().isPrimitive() || obj.getClass().isEnum(); } }这段代码的核心逻辑很清晰先处理 null再处理简单类型然后处理集合和 Map最后逐字段递归。pathPrefix用来保留嵌套字段路径比如address.city。isSimpleType决定了哪些类型不继续拆字段否则反射会尝试拆解 String 和 Integer 的内部结构那会得到一堆无意义的差异。这里真正容易踩坑的地方是setAccessible(true)。如果项目运行在 JDK 17 以上并且使用了强封装模块对某些类的私有字段访问可能抛出InaccessibleObjectException。所以这套反射比较方案更适用于普通业务 POJO不要把 JDK 内部类和第三方框架类传进去。生产环境如果遇到访问异常可以加一个 try-catch 并返回“无法访问字段”的差异而不是让整个比较失败。5. 完整示例与代码实现有了核心工具类下面用一个完整的示例来演示。先定义一个用户视图对象包含用户名、年龄、地址对象和一个标签列表。// 文件路径src/com/example/compare/UserInfo.java import java.util.List; public class UserInfo { private String name; private Integer age; private Address address; private ListString tags; public UserInfo() { } public UserInfo(String name, Integer age, Address address, ListString tags) { this.name name; this.age age; this.address address; this.tags tags; } public String getName() { return name; } public void setName(String name) { this.name name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age age; } public Address getAddress() { return address; } public void setAddress(Address address) { this.address address; } public ListString getTags() { return tags; } public void setTags(ListString tags) { this.tags tags; } public static class Address { private String city; private String street; public Address() { } public Address(String city, String street) { this.city city; this.street street; } public String getCity() { return city; } public void setCity(String city) { this.city city; } public String getStreet() { return street; } public void setStreet(String street) { this.street street; } Override public String toString() { return Address{ city city \ , street street \ }; } } }再写集合比较工具类。集合比较最常见的需求是从两个集合中找出新增、删除、不变的元素。注意这件事不能只看集合的toString因为 Set 和 List 的顺序语义不一样。List 可能允许重复且顺序敏感Set 通常在业务语义上不关心顺序。做集合差异比较时要先明确你要的是“元素差集”还是“顺序差集”。// 文件路径src/com/example/compare/CollectionCompareUtil.java import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; public class CollectionCompareUtil { /** * 基于某个唯一键比较两个列表。 * 返回一个简单的结果对象包含新增、删除、未变动元素。 */ public static T, K DiffResultT compareByKey(ListT oldList, ListT newList, FunctionT, K keyExtractor) { MapK, T oldMap oldList.stream() .collect(Collectors.toMap(keyExtractor, Function.identity(), (a, b) - a)); MapK, T newMap newList.stream() .collect(Collectors.toMap(keyExtractor, Function.identity(), (a, b) - a)); ListT added new ArrayList(); ListT removed new ArrayList(); ListT unchanged new ArrayList(); for (Map.EntryK, T entry : newMap.entrySet()) { if (!oldMap.containsKey(entry.getKey())) { added.add(entry.getValue()); } else { unchanged.add(entry.getValue()); } } for (Map.EntryK, T entry : oldMap.entrySet()) { if (!newMap.containsKey(entry.getKey())) { removed.add(entry.getValue()); } } return new DiffResult(added, removed, unchanged); } public static class DiffResultT { private final ListT added; private final ListT removed; private final ListT unchanged; public DiffResult(ListT added, ListT removed, ListT unchanged) { this.added added; this.removed removed; this.unchanged unchanged; } public ListT getAdded() { return added; } public ListT getRemoved() { return removed; } public ListT getUnchanged() { return unchanged; } Override public String toString() { return DiffResult{ added added , removed removed , unchanged unchanged }; } } }多字段排序是另一个非常常见的“比较”需求。如果UserInfo没有实现Comparable我们可以用Comparator.comparing链式完成多字段排序。在写排序比较器时要注意 null 值的处理。Comparator.nullsLast可以安全地把 null 值排到后面。// 文件路径src/com/example/compare/SortUtil.java import java.util.ArrayList; import java.util.Comparator; import java.util.List; public class SortUtil { public static ListUserInfo sortByAgeAndName(ListUserInfo source) { ListUserInfo result new ArrayList(source); result.sort(Comparator .comparing(UserInfo::getAge, Comparator.nullsLast(Integer::compareTo)) .thenComparing(UserInfo::getName, Comparator.nullsLast(String::compareTo))); return result; } }最后写一个Main演示类把所有能力串起来// 文件路径src/com/example/compare/Main.java import com.example.compare.CollectionCompareUtil.DiffResult; import java.util.Arrays; import java.util.List; public class Main { public static void main(String[] args) { UserInfo oldUser new UserInfo( 张三, 30, new UserInfo.Address(北京, 中关村大街), Arrays.asList(vip, active) ); UserInfo newUser new UserInfo( 张三, 31, new UserInfo.Address(上海, 张江路), Arrays.asList(vip, blocked) ); System.out.println( 对象字段差异 ); ListDiffField diffs ObjectDiffUtil.compare(oldUser, newUser); diffs.forEach(System.out::println); System.out.println( 集合差异按 key ); ListUserInfo oldList Arrays.asList( new UserInfo(a, 20, null, null), new UserInfo(b, 21, null, null) ); ListUserInfo newList Arrays.asList( new UserInfo(b, 21, null, null), new UserInfo(c, 22, null, null) ); DiffResultUserInfo diffResult CollectionCompareUtil.compareByKey( oldList, newList, UserInfo::getName ); System.out.println(diffResult); System.out.println( 多字段排序 ); ListUserInfo userList Arrays.asList( new UserInfo(b, 22, null, null), new UserInfo(a, 22, null, null), new UserInfo(c, 20, null, null) ); ListUserInfo sorted SortUtil.sortByAgeAndName(userList); sorted.forEach(u - System.out.println(u.getName() : u.getAge())); } }注意UserInfo.Address是内部静态类构造时按new UserInfo.Address(...)使用。在ObjectDiffUtil中通过getDeclaredFields只能拿到UserInfo直接声明的字段不会自动获取父类字段。如果目标对象有父类字段需要额外用循环遍历父类。这个设计是刻意的通常建议把父类字段也纳入比较否则很容易漏比较。以上三个代码示例覆盖了本文最重要的三类能力。代码不要求多复杂但它给出了一个可以直接运行的骨架。你可以复制到自己的工程里然后根据业务字段改一改。6. 运行结果与效果验证按照前面的命令行方式编译运行预期会看到类似下面的结果 对象字段差异 DiffField{pathage, fieldNameage, oldValue30, newValue31} DiffField{pathaddress, fieldNameaddress, oldValueAddress{city北京, street中关村大街}, newValueAddress{city上海, street张江路}} DiffField{pathtags, fieldNametags, oldValue[vip, active], newValue[vip, blocked]} 集合差异按 key DiffResult{added[UserInfo{...namec}], removed[UserInfo{...namea}], unchanged[UserInfo{...nameb}]} 多字段排序 c : 20 a : 22 b : 22这里需要重点看address的差异。因为UserInfo.Address不是简单类型反射比较会进入Address内部继续比较city和street字段但由于Address没有重写toString在结果里显示成对象的toString形式。如果你想看到address.city这样的路径可以给Address加一个toString或者把UserInfo.Address设计成普通类反射一样可以递归。当前输出按Address整体差异处理是因为前面代码对集合和 Map 做了简化对普通对象是递归的但控制台展示时调用了DiffField.toString所以显示为对象文本。验证成功的标准很简单所有字段差异都能正确列出新增、删除、不变的元素都能被区分排序结果符合预期。如果运行时发现反射比较抛异常第一步应该看异常堆栈中的“类名”和“字段名”。大多数情况下异常来源是setAccessible或字段类型不可访问。然后再确认传入对象是否包含了非业务 POJO 的类。还有一个容易被忽略的验证点ObjectDiffUtil.compare对oldObj为 null 或newObj为 null 做了特殊处理。所以调用方不需要先判空。但如果你在业务代码里用Objects.equals(oldUser.getAddress(), newUser.getAddress())来判断嵌套对象是否变化那和返回差异列表是不同的机制不要混用。7. 常见问题与排查思路反射版的比较模块容易遇到下面这些问题。排查时不要凭感觉先看日志再核对类型最后看字段是否在预期范围内。问题现象可能原因排查方式解决方案启动或运行时报InaccessibleObjectException反射访问了强封装模块中的类查看异常栈中的类名确认是否业务 POJO如果是 JDK 类改用显式比较如果是业务 POJO检查包名和模块配置比较结果出现StackOverflowError对象存在循环引用或嵌套层级过深打印递归深度检查对象图限制最大深度或对已知循环引用的字段跳过集合字段总是显示差异实际数据没变集合顺序不同导致 toString 不同单独打印两个集合的 toString 对比改用按 key 比较或排序后再比较父类字段没有被比较getDeclaredFields()只返回当前类字段打印字段列表确认父类字段缺失循环遍历父类 Class 获取字段null 值比较误判一个字段是 null另一个是空字符串打印两边实际值在工具类中定义“空值归一化”规则输出差异里出现不相关字段反射遍历到了serialVersionUID等字段打印字段名检查实体是否有额外静态字段增加字段过滤名单比较性能很差每次都反射获取字段列表用 profiler 查看耗时缓存字段列表或使用显式比较日志中输出敏感字段实体包含密码、手机号等敏感字段检查差异输出内容增加敏感字段黑名单脱敏后写入日志最容易在项目里翻车的是“集合字段总是显示差异”这个问题。原因很简单很多实体类的List字段允许元素顺序变化但业务语义上顺序不重要。此时用toString比较会得到 false进而输出一条无意义的差异。比较好的处理方式是在比较模块里增加一个配置哪些字段按集合比较器处理哪些字段直接跳过。另一个常见问题是“字段访问权限”。如果实体类使用 Lombok 的Data编译后字段仍然是 private反射比较没有问题。但如果你把实体类放在模块化的 JPMS 工程里并且没有开放自身模块的包导出setAccessible会被 Java 模块系统拦下来。遇到这种情况要么把所有业务 POJO 放在非模块化的 classpath要么把比较逻辑从反射改为调用 getter 方法。用 getter 的好处是不受字段访问限制影响坏处是代码量会增加。这也是为什么我说“反射适合工具类显式比较适合生产关键路径”。8. 最佳实践与工程建议反射比较模块是一把双刃剑。用好了可以是数据排查的利器用不好就是生产事故的隐藏雷。结合实际项目经验我给出几条工程建议。第一先用显式比较再用反射兜底。如果你要比较的是一个字段非常稳定的核心对象建议直接写一个BusinessComparator逐个字段用Objects.equals比较并手动拼接差异。这样做虽然笨但可读性强、不容易误报。反射比较更适合字段频繁变化、类型多种多样的对象。换句话说把反射比较放在工具层把显式比较放在业务层两者不冲突。第二差异结果必须带上字段路径。只输出字段名的问题是嵌套对象里可能有多个同名子字段。比如shippingAddress.city和billingAddress.city如果只输出city调用方根本不知道是哪个地址变了。带路径之后排查效率会大幅提升。本文的DiffField用path字段解决了这个问题建议你保留这个设计。第三一定要做深度限制和循环引用检测。反射比较最危险的场景是两个对象互相引用。一个常见的例子是父子节点对象父节点持有子节点列表子节点又持有 parent 引用。如果不控制深度递归会无限蔓延。除了深度限制更稳妥的做法是维护一个IdentityHashMap记录已经访问过的对象组合。第四过滤敏感字段。比较模块经常用于审计和日志而实体里很可能有密码、手机号、身份证号、内部备注等敏感字段。当比较结果被打印到日志时这些字段会明文暴露。建议在比较工具里支持一个“忽略字段集合”或者在输出结果之前做一次脱敏映射。至少要做到默认不输出标记为敏感注解的字段。第五性能优化。反射获取字段列表是比较耗时的尤其是每次请求都调用时。可以引入一个静态缓存用Class作为 key 缓存字段列表。但要注意缓存字段列表后如果类被多个 ClassLoader 加载容易出现内存泄漏所以通常使用ClassValue或弱引用缓存。对大多数 Web 应用来说简单ConcurrentHashMap配合少量 POJO 也够用。第六集合比较要明确“唯一键”。compareByKey这类方法依赖调用方提供 keyExtractor。建议在业务层用 “业务唯一键”比较比如订单号、用户 ID、配置 key。不要用对象的默认equals去判断重复因为默认 equals 比较的是内存地址除非你正确重写了equals和hashCode。集合差集计算如果不指定 key很可能得到的是“内存地址不同”的非预期结果。第七保持比较模块无状态。不要在比较工具类里持有全局状态。工具方法应该以静态方法或 Spring 单例 Bean 提供但内部不要保存“上一次比较结果”。状态越少越容易测试和并行调用。第八单元测试要覆盖 null、嵌套、循环引用三个场景。比较模块看起来简单但它是最容易被回归 bug 影响的基础工具。建议至少写以下用例两个相同对象无差异一个空对象一个非空对象有差异嵌套对象子字段变化能识别列表按 key 比较不出错排序比较器对 null 兼容。9. 总结与后续学习方向这篇文章从一个真实排查痛点出发给出了 Java 中比较模块的完整设计思路和可运行示例。核心结论可以归纳为三点第一比较模块的价值不是判断“是否相等”而是输出“具体差异”第二对象差异比较推荐“显式比较 反射兜底 集合按 key 比较”的组合方案第三反射比较要特别注意循环引用、敏感字段、字段路径和性能问题。如果你想把比较模块做得更健壮下一步可以考虑三个方向一是接入 JSON 序列化把对象序列化为树形结构后做树 diff这样可以输出更精确的新增、删除、修改语义二是将比较结果扩展为“变更事件”在数据更新后自动发布事件供审计、同步、通知等下游消费三是把比较配置外部化例如标记哪些字段忽略、哪些字段脱敏、哪些集合按顺序比较避免每次改需求都改代码。在实际项目中我强烈建议不要一开始就追求“万能比较模块”。先从一个具体对象开始跑通一条业务链路再看它能不能复用。很多同学一上来就写好几百行反射工具类结果放到项目里后被各种特殊类型、循环引用、父类字段问题折磨得改来改去。真正可维护的比较模块往往是业务规则和结构遍历共同作用的结果通用逻辑负责遍历业务规则负责取舍。把这个边界想清楚你的比较模块才算真正落地。
网站建设高端定制企业官网