【实用技巧】【探讨】Java 中比较两个对象的差异
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<>(); // 先比较父类的字段 除了 Object Class 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; @Override public 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 小结
暂时先写一版哈,还请大佬们指点一二,看看核心的比较逻辑对不对哈。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
2024-01-09 【算法】【线性表】【链表】链表求和