【实用技巧】【探讨】Java 中比较两个对象的差异

news/2025/1/9 21:26:00/文章来源:https://www.cnblogs.com/kukuxjx/p/18662908

1  前言

大家平时写业务代码的时候,应该能感知到哪些是基础配置数据,哪些是实例数据。比如营销里的活动信息、促销信息就属于配置型数据,基于活动带来的订单参与活动信息属于实例数据。比如一些规则信息、流程信息等类似一种版本的概念。那么版本跟版本之间的差异、以及创建新版本的时候,什么也没变化到底生不生成版本信息呢?如何知道版本跟版本之间有没有差异呢?是不是就要比对两个版本之间的数据。

版本和版本之间的比对,我们大概能想到的方式:

  • Json化后的比较,JSON常用的有 Jackson、Gson、Fastjson,这个的比较方式下节说
  • Java实体间的比较,比如从数据库里取出来的实体信息和当前请求的实体信息间的比较,本节主要看下这个
  • 实体按某种规则顺序化后的字符串或者某种规则文本的比较,比如将你的实体信息按字典或者某种规则化后得到一个字符串或者规则文本进行比较,有点类似 Json,Json本身也是一种有规则的文本,但这种方式更偏向于我们自己定义的规则而非Json,所以我单独拿出来

大概我能想到的是上边这三种方式,然后我们本节主要看下第二种方式,Java 实体间的比较。

2  实践

2.1  代码

我这里就直接贴代码了哈,大概定义了三个类:

  • CompareResultInfo 比较结果实体信息
  • CompareDetailInfo 差异明细信息
  • CompareInfoUtil 入口以及核心比较类

CompareResultInfo 比较结果实体信息:

@Data
public class CompareResultInfo {// 比较结果 true 表示两个对象相等,false 表示两个对象不相等private boolean equalFlag;// 比较开始时间private long startCompareTime;// 比较结束时间private long endCompareTime;// 比较总耗时private long totalCompareTime;// 比较差异详情private List<CompareDetailInfo> compareDetailInfos;
}

CompareDetailInfo 差异明细信息:

@Builder
@Data
public class CompareDetailInfo {// 差异字段private Field field;// 旧值private Object oldVal;// 新值private Object newVal;
}

CompareInfoUtil 入口以及核心比较类:

/*** 比较工具类* @author kuku* @param*/
@Data
public class CompareInfoUtil {// 入口public static CompareResultInfo compareObj(Class clz, Object oldVal, Object newVal) {// 结果信息CompareResultInfo resultInfo = new CompareResultInfo();// 开始比较
        startCompare(resultInfo);try {// 基础检查if (clz == null) throw new RuntimeException("clazz,比较的类不能为空");if (oldVal == null) throw new RuntimeException("oldVal,旧值不能为空");if (newVal == null) throw new RuntimeException("newVal,新值不能为空");// 执行比较List<CompareDetailInfo> compareDetailInfos = doCompareObj(clz, oldVal, newVal);// 差异resultInfo.setEqualFlag(compareDetailInfos.size() <= 0);resultInfo.setCompareDetailInfos(compareDetailInfos);return resultInfo;} finally {// 结束比较
            endCompare(resultInfo);}}private static void startCompare(CompareResultInfo resultInfo) {// 设置开始时间
        resultInfo.setStartCompareTime(System.currentTimeMillis());}private static void endCompare(CompareResultInfo resultInfo) {// 设置结束时间
        resultInfo.setEndCompareTime(System.currentTimeMillis());// 计算耗时long startCompareTime = resultInfo.getStartCompareTime();long endCompareTime = resultInfo.getEndCompareTime();long total = endCompareTime - startCompareTime;resultInfo.setTotalCompareTime(total);System.out.println(String.format("开始时间:%s,结束时间:%s,耗费时间(毫秒):%s", startCompareTime, endCompareTime, total));}private static List<CompareDetailInfo> doCompareObj(Class clz, Object oldVal, Object newVal) {List<CompareDetailInfo> currentCompareDetailInfoList = new ArrayList<>();// 先比较父类的字段 除了 ObjectClass superclass = clz.getSuperclass();if (superclass != null && superclass != Object.class) {List<CompareDetailInfo> parentInfos = doCompareObj(superclass, oldVal, newVal);if (CollectionUtils.isNotEmpty(parentInfos)) {currentCompareDetailInfoList.addAll(parentInfos);}}// 获取当前类中的字段Field[] fields = clz.getDeclaredFields();// 逐个比较for (Field field : fields) {field.setAccessible(true);// 当前字段类型Class<?> fieldType = field.getType();Object ov = null;Object nv = null;try {ov = field.get(oldVal);nv = field.get(newVal);} catch (Exception e) {// ignore
            }// 比较当前属性boolean equalFlag = doCompareAttr(fieldType, ov, nv);// 不相等 汇总明细信息if (!equalFlag) {describeOneCompareDetail(currentCompareDetailInfoList, field, ov, nv);}}return currentCompareDetailInfoList;}private static boolean doCompareAttr(Class clz, Object oldVal, Object newVal) {boolean res = true;// 两个都为空 直接略过if (oldVal == null && newVal == null) {return res;}// 有一个为空if (oldVal == null || newVal == null) {res = false;} else {if (clz.isArray()) {res = doCompareArr(clz, oldVal, newVal);} else if (oldVal instanceof Collection) {res = doCompareCollection(clz, oldVal, newVal);} else if (oldVal instanceof Map) {res = doCompareMap(clz, oldVal, newVal);} else {// 为什么是 ov.getClass 不是 fieldType 呢// 因为当是基本数据类型的时候 比如 int 它的 fieldType 是 int 类// 没重写 equals 而得到的 ov nv 都是包装后的对象 所以直接拿对象的 getClass()if (hasOverriddenEquals(oldVal.getClass())) {res = oldVal.equals(newVal);} else {List<CompareDetailInfo> compareDetailInfos = doCompareObj(clz, oldVal, newVal);if (CollectionUtils.isNotEmpty(compareDetailInfos)) {res = false;}}}}return res;}/*** 比较 Map 类型的* @param clz* @param ov* @param nv* @return*/private static boolean doCompareMap(Class<?> clz, Object ov, Object nv) {boolean res = true;Map ov1 = (Map) ov;Map nv1 = (Map) nv;// 长度不相等直接略过if (ov1.size() != nv1.size()) {res = false;return res;}Set keys = ov1.keySet();for (Object key : keys) {Object o1 = ov1.get(key);Object o2 = nv1.get(key);res = doCompareAttr(o1.getClass(), o1, o2);if (!res) {break;}}return res;}/*** 比较集合类型的 这里我暂时直接把集合转成了数组 直接调数组的方法* @param clz* @param ov* @param nv* @return*/private static boolean doCompareCollection(Class<?> clz, Object ov, Object nv) {Collection ovC = (Collection) ov;Collection nvC = (Collection) nv;return doCompareArr(clz, ovC.toArray(), nvC.toArray());}/*** 比较数组类型的* @param clz* @param ov* @param nv* @return*/private static boolean doCompareArr(Class clz, Object ov, Object nv) {boolean res = true;String name = clz.getName();switch (name) {case "[Z": {boolean[] ov1 = (boolean[]) ov;boolean[] nv1 = (boolean[]) nv;res = Arrays.equals(ov1, nv1);}; break;case "[B": {byte[] ov1 = (byte[]) ov;byte[] nv1 = (byte[]) nv;Arrays.sort(ov1);Arrays.sort(nv1);res = Arrays.equals(ov1, nv1);}; break;case "[C": {char[] ov1 = (char[]) ov;char[] nv1 = (char[]) nv;Arrays.sort(ov1);Arrays.sort(nv1);res = Arrays.equals(ov1, nv1);}; break;case "[S": {short[] ov1 = (short[]) ov;short[] nv1 = (short[]) nv;Arrays.sort(ov1);Arrays.sort(nv1);res = Arrays.equals(ov1, nv1);}; break;case "[I": {int[] ov1 = (int[]) ov;int[] nv1 = (int[]) nv;Arrays.sort(ov1);Arrays.sort(nv1);res = Arrays.equals(ov1, nv1);}; break;case "[J": {long[] ov1 = (long[]) ov;long[] nv1 = (long[]) nv;Arrays.sort(ov1);Arrays.sort(nv1);res = Arrays.equals(ov1, nv1);}; break;case "[F": {float[] ov1 = (float[]) ov;float[] nv1 = (float[]) nv;Arrays.sort(ov1);Arrays.sort(nv1);res = Arrays.equals(ov1, nv1);}; break;case "[D": {double[] ov1 = (double[]) ov;double[] nv1 = (double[]) nv;Arrays.sort(ov1);Arrays.sort(nv1);res = Arrays.equals(ov1, nv1);}; break;default: {// 转成数组Object[] ovArr = (Object[]) ov;Object[] nvArr = (Object[]) nv;// 长度不一样直接回去if (ovArr.length != nvArr.length) {res = false;return res;}// 数组元素没有顺序性的话 直接略过// 因为没顺序 没法比较到底哪个跟哪个进行比较呢if (!(ovArr[0] instanceof Comparable)) {return res;}// 排序
                Arrays.sort(ovArr);Arrays.sort(nvArr);// 逐个比较呗for (int i = 0; i < ovArr.length; i++) {Object o1 = ovArr[i];Object o2 = nvArr[i];// 断路 发现不相等的直接结束res = doCompareAttr(o1.getClass(), o1, o2);if (!res) {break;}}}}return res;}/*** 判断是否重写了 equals 方法* @param clazz* @return*/private static boolean hasOverriddenEquals(Class<?> clazz) {try {Method equalsMethod = clazz.getMethod("equals", Object.class);return !equalsMethod.isSynthetic();} catch (NoSuchMethodException e) {return false;}}/*** 描述一个比较结果* @param list* @param field* @param oldVal* @param newVal*/private static void describeOneCompareDetail(List<CompareDetailInfo> list, Field field, Object oldVal, Object newVal) {CompareDetailInfo detailInfo = CompareDetailInfo.builder().field(field).oldVal(oldVal).newVal(newVal).build();list.add(detailInfo);}}

暂时先写了一版,还有些考虑还没实现,比如 ignoreFields 忽略哪些属性的比较因为像有些创建时间等没必要比较的需要忽略,还有差异信息的汇总不够直白,最后要呈现给用户还需要再转译一下等。

2.2  测试效果

我这里简单拿了三个类进行了一下实验,我贴出来,方便大家实验:

Base 基础类:

@Data
public class Base {// 创建时间private LocalDateTime createTime;// 创建人private String createUserName;// 更新时间private LocalDateTime modifyTime;// 更新人private String modifyUserName;// 逻辑删除标志private Boolean deleted;
}

User 用户类:

@Data
@Builder
public class User extends Base{// 姓名private String name;// 年龄private Integer age;// 性别private Short gender;// 其他类型实验private int num1;private boolean boolean1;private float float1;private char char1;private String[] strArr;private int[] intArr;private Integer[] integerArr;private UserSub[] userArr;private List<UserSub> userList;
}

UserSub 关联类:

@Data
public class UserSub implements Comparable<UserSub> {private String subName;private Integer subAge;@Overridepublic int compareTo(UserSub o) {String oSubName = o.getSubName();if (this.subName == null && oSubName == null) {return 0;}if (this.subName == null) {return -1;}if (oSubName == null) {return 1;}return this.subName.compareTo(oSubName);}}

测试类:

/*** @author: xjx* @description*/
public class CompareObj {public static void main(String[] args) {User oldVal = User.builder().build();User newVal = User.builder().build();oldVal.setCreateTime(LocalDateTime.now());oldVal.setStrArr(new String[]{"1"});newVal.setStrArr(new String[]{"12"});oldVal.setIntArr(new int[]{1,2});newVal.setIntArr(new int[]{1,2});oldVal.setIntegerArr(new Integer[]{1, 2});newVal.setIntegerArr(new Integer[]{1, 2});oldVal.setUserList(Lists.newArrayList(new UserSub()));newVal.setUserList(Lists.newArrayList(new UserSub()));UserSub userSub = new UserSub();userSub.setSubName("111");oldVal.setUserArr(new UserSub[]{userSub});newVal.setUserArr(new UserSub[]{new UserSub()});CompareResultInfo resultInfo = CompareInfoUtil.compareObj(User.class, oldVal, newVal);List<CompareDetailInfo> detailInfoList = resultInfo.getCompareDetailInfos();for (CompareDetailInfo detailInfo : detailInfoList) {System.out.println(detailInfo);}}
}

目前效果:

结果后续还需要转译一下,翻译给用户能看懂的数据信息。

3  小结

暂时先写一版哈,还请大佬们指点一二,看看核心的比较逻辑对不对哈。 

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

主机与虚拟机互ping不通的解决办法

一、主机ping不通虚拟机,但虚拟机可以ping主机 解决办法:二、虚拟机ping不通主机,但主机可以ping虚拟机 解决办法: 这里说的第一点我不知道,我是虚拟机可以ping通百度,之前虚拟机有过相关配置 我这里主要是win11电脑网络连接这里开启防火墙下面简单的说明下Vmware的网络连…

Win32汇编学习笔记09.SEH和反调试

Win32汇编学习笔记09.SEH和反调试-C/C++基础-断点社区-专业的老牌游戏安全技术交流社区 - BpSend.net SEH - structed exception handler 结构化异常处理 跟筛选一样都是用来处理异常的,但不同的是 筛选器是整个进程最终处理异常的函数,但无法做到比较精细的去处理异常(例如处理…

【Windows攻防】Windows注册表 IFEO注入

介绍 IFEO 注入攻击是一种基于注册表的攻击技术,几乎可以保证以登录用户甚至管理员/系统用户的身份执行代码。在任何你可以想象的用例中,如果你想要将你的有效载荷绑定到 Windows 平台上二进制文件的“启动”,IFEO(图像文件执行选项)可能是你最好的选择。这是一种开发人员…

[Java] 计算Java对象大小

序在Java应用程序的性能优化场景中,时常需要考虑Java对象的大小,以便评估后,进一步提出优化方案:占用内存的大小。(比如 本地内存) 对象数据在网络传输中占用的网络带宽 对象数据在存储时占用的磁盘空间 ...概述 对象大小如何计算对象大小包括俩部分的内容,对象头和对象…

并行前缀(Parallel Prefix)加法器

并行前缀(Parallel Prefix)加法器 并行前缀加法器的基本介绍 二进制加法器是目前数字计算单元中的重要模块,基础的加法器架构包括行波进位加法器(Ripple Carry Adder),超前进位加法器(Carry Look-Ahead Adder),进位选择加法器(Carry Select Adder)等。加法器的进位传…

科技风?写实风?教你设置多风格三维地图

概述 三维地图通过高度、深度、立体感等表现形式,能够真实还原地形地貌、城市建筑和空间结构。相比二维地图,它能够更清晰地展示复杂的地理数据,帮助用户快速理解空间关系,如地形起伏、建筑高度等。在实际应用中,我们可以将不同风格的三维地图作为项目的主体元素进行展示,…

【模拟电子技术】03-PN与二极管的特性

【模拟电子技术】03-PN与二极管的特性上节中有提到对PN结施加反向电压时,会使得PN结所形成的势垒增加,阻止多子到另一边。在掺杂浓度比较低的时候,外加电场加强,中间的耗尽层会加长,变成了一个粒子加速器,自由电子进去后不断加速。直到某一电场强度时,粒子加速足够大的时…

NocoBase 本周更新汇总:支持大规模数据量的导入和导出

本周更新包括:支持大规模数据量的导入和导出等。汇总一周产品更新日志,最新发布可以前往我们的博客查看。 NocoBase 目前更新包括的版本更新包括三个分支:main ,next和 develop。main :截止目前最稳定的版本,推荐安装此版本。 next:包含即将发布的新功能,经过初步测试的…

MSSQL:DBLINK连接oracle 19

无法为该请求检索数据。(Microsoft.SqlServer.Management.Sdk.Sfc)其他信息:执行Transact-SQL语句或批处理时发生了异常。(Microsoft.SqlServer.ConnectionInfo)在与SQL Server 建立连接时出现与网络相关的特定于实例的错误。未找到或无法访问服务器。请验证实例名称是否正…

五款强大报表软件助力企业提升数据分析效率

本文将为大家介绍五款功能强大的报表软件,包括山海鲸报表、JReport、Power BI、Zoho Analytics 和 SAP Crystal Reports。这些工具各具特色,能够帮助企业快速生成数据报表并进行深度分析。无论是数据可视化、报表定制、自动化生成还是与其他系统的集成,它们都能为企业的决策…

Linq中的设置操作 (C#):Distinct 和 DistinctBy、Except 和 ExceptBy、Intersect 和 IntersectBy、Union 和 UnionBy

LINQ 中的集运算是指根据相同或单独集合中是否存在等效元素来生成结果集的查询运算。 注:这些示例使用 System.Collections.Generic.IEnumerable<T> 数据源。 基于 System.Linq.IQueryProvider 的数据源使用 System.Linq.IQueryable<T> 数据源和表达式树。 表达式…

2025多校冲刺省选模拟赛3

过于困难,直接放弃2025多校冲刺省选模拟赛3\(T1\) A. 等差 \(100pts/100pts\)考虑哈希,每 \(k\) 个作为一组与上一组统一计算。取 \(Base>\) 值域时用高精度来存储并判断的正确性显然。观察到可行的最小的 \(k\) 单调不降,不妨直接枚举答案。暴力实现时间复杂度为 \(O(n…