實現兩個 JSON 物件的對比
阿新 • • 發佈:2022-05-06
問題描述
在 Java 中,對於兩個物件的對比,如果沒有重寫 equals
方法,那麼將會預設使用 Object
預設的 equals
方法來比較兩個物件。這種比較方式是通過比較兩個物件的記憶體地址是否是一致的來進行判斷的
然而,假設這麼一種需求:比對兩個物件的所有欄位,同時使用某種形式記錄不同的欄位屬性,已經這個屬性對應的新舊值。
對於這個需求,可以分為以下幾個步驟進行處理:
- 首先第一步,如何判斷物件的屬性是否相等
-
在 Java 中可以通過反射的方式來獲取物件的相關屬性,對於基本的資料型別,如
integer
、long
、double
等以及它們的裝箱類,都已經重寫了equals
方法,因此可以直接通過equals
-
如果屬性是物件型別,那麼需要分為幾種情況討論:常用物件(如 String、Date、BigDecimal 等)、陣列型別物件、一般集合物件、一般 Java 物件。對於常用物件,同樣已經自己重寫了
euqals
方法,直接使用equals
方法進行比較即可;對於陣列型別物件和一般集合物件,則不得不對整個集合中的所有元素進行比較;而對於一般的 Java 物件則可以遞迴地對屬性進行比較
- 第二步,如何記錄不同的屬性
按照一般的 Java 訪問類的方式,如 com.example.demo.Solution
,可以使用類似的方式來記錄不同的屬性,同樣以 .
來分隔不同的物件屬性。
對於陣列型別和集合型別,可能需要記錄不同的元素位置,因此可以新增額外的索引資訊,可以使用 #
分隔
- 第三步,如何記錄新舊值
使用一個新的物件 Node
作為當前屬性的值即可
具體實現
import com.google.common.base.Objects; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.math.BigInteger; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; import static java.util.concurrent.ThreadLocalRandom.current; /** * @author xhliu * @create 2022-03-18-15:00 **/ //@SpringBootTest public class DifferTool { static final Set<Class<?>> BASIC_CLASS_SET = new HashSet<>(); static { BASIC_CLASS_SET.add(Number.class); BASIC_CLASS_SET.add(Byte.class); BASIC_CLASS_SET.add(Short.class); BASIC_CLASS_SET.add(Integer.class); BASIC_CLASS_SET.add(Long.class); BASIC_CLASS_SET.add(Float.class); BASIC_CLASS_SET.add(Double.class); BASIC_CLASS_SET.add(BigDecimal.class); BASIC_CLASS_SET.add(BigInteger.class); BASIC_CLASS_SET.add(String.class); BASIC_CLASS_SET.add(Character.class); BASIC_CLASS_SET.add(Date.class); BASIC_CLASS_SET.add(java.sql.Date.class); BASIC_CLASS_SET.add(LocalDateTime.class); BASIC_CLASS_SET.add(LocalDate.class); BASIC_CLASS_SET.add(Instant.class); } final static class Node<T> { final T oldVal, newVal; public Node(T oldVal, T newVal) { this.oldVal = oldVal; this.newVal = newVal; } @Override public String toString() { return "Node{" + "oldVal=" + oldVal + ", newVal=" + newVal + '}'; } } /** * 比較兩個物件之間的每個屬性是否完全一致,如果不一致, * 則使用一個 Map 記錄不同的屬性位置以及原有舊值和新值 * * @param o1 : 舊有資料物件 * @param o2 : 新的資料物件 * @return : 如果兩個物件的屬性完全一致,返回 true,否則,返回 false */ boolean compObject(Object o1, Object o2) { Map<String, Node<Object>> map = compare(o1, o2); return map.size() == 0; } /** * 比較兩個物件,通過反射的方式遞迴地對每個欄位進行比較,對於不同的欄位,將會記錄當前的遞迴欄位屬性, * 同時通過一個 {@code Node} 物件來記錄兩者之間該屬性的舊值和新值 * * @param oldObj : 舊值物件 * @param newObj : 新值物件 * @return 記錄兩個物件不同屬性的 Map,Map 中存有的 {@code key} 應當是以 {@code .} 的分隔字串形式 */ Map<String, Node<Object>> compare(Object oldObj, Object newObj) { Map<String, Node<Object>> map = new HashMap<>(); dfs(oldObj, newObj, "", map); return map; } static boolean dfs(Object o1, Object o2, String prefix, Map<String, Node<Object>> map) { if (o1 == null && o2 == null) return true; if (o1 == null) { map.put(prefix, new Node<>(null, o2)); return false; } if (o2 == null) { map.put(prefix, new Node<>(o1, null)); return false; } checkParams(o1.getClass() != o2.getClass(), "o1 和 o2 的物件型別不一致"); boolean res = true; // 檢查當前物件的屬性以及屬性物件的子屬性的值是否一致 Field[] fields = o1.getClass().getDeclaredFields(); for (Field field : fields) { int modifiers = field.getModifiers(); if ((modifiers & Modifier.STATIC) != 0) continue; // 過濾靜態修飾符修飾的欄位 field.setAccessible(true); String curFiled = prefix + (prefix.length() > 0 ? "." : "") + field.getName(); try { final Class<?> fieldClass = field.getType(); Object v1 = field.get(o1), v2 = field.get(o2); if (checkHandle(checkBasicType(v1, v2, fieldClass, curFiled, map))) continue; if (checkHandle(checkCollection(v1, v2, fieldClass, curFiled, map))) continue; if (checkHandle(checkMap(v1, v2, fieldClass, curFiled, map))) continue; if (checkHandle(checkEnum(v1, v2, fieldClass))) continue; res &= dfs(v1, v2, curFiled, map); } catch (IllegalAccessException e) { e.printStackTrace(); } } return res; } final static int EQUALS = 1 << 1; // 表示比較的物件的當前屬性相等 final static int NO_EQUALS = 1 << 2; // 表示當前物件的型別能夠進行處理,但是兩個物件值並不相等 final static int DISABLE = 1 << 3; // 表示當前傳入的物件該方法無法進行處理 final static int PRIME = 51; // 一個比較正常的質數,這個質數將會作為進位數來計算物件的 hash 值 private static boolean checkHandle(int handle) { return handle == EQUALS || handle == NO_EQUALS; } private static int checkBasicType( Object v1, Object v2, Class<?> fieldClass, String curFiled, Map<String, Node<Object>> map ) { if (isBasicType(fieldClass)) { if (v1 == null && v2 == null) return EQUALS; if (v1 == null) { map.put(curFiled, new Node<>(null, v2)); return NO_EQUALS; } if (v2 == null) { map.put(curFiled, new Node<>(v1, null)); return NO_EQUALS; } if (equalsObj(v1, v2)) return 1 << 1; map.put(curFiled, new Node<>(v1, v2)); return NO_EQUALS; } return DISABLE; } private static int checkCollection( Object v1, Object v2, Class<?> fieldClass, String curFiled, Map<String, Node<Object>> map ) { if (isCollection(fieldClass)) return equalsCollect(v1, v2, curFiled, map) ? EQUALS : NO_EQUALS; return DISABLE; } private static int checkMap( Object v1, Object v2, Class<?> fieldClass, String curFiled, Map<String, Node<Object>> map ) { if (isMap(fieldClass)) return equalsMap((Map<?, ?>) v1, (Map<?, ?>) v2, curFiled, map) ? EQUALS : NO_EQUALS; return DISABLE; } private static int checkEnum( Object v1, Object v2, Class<?> fieldClass ) { if (isEnum(fieldClass)) { return equalsEnum((Enum<?>) v1, (Enum<?>) v2) ? EQUALS : NO_EQUALS; } return DISABLE; } static boolean isBasicType(Class<?> c) { if (c.isPrimitive()) return true; return BASIC_CLASS_SET.contains(c); } static boolean isCollection(Class<?> c) { return Collection.class.isAssignableFrom(c); } static boolean isMap(Class<?> c) { return Map.class.isAssignableFrom(c); } static boolean isEnum(Class<?> c) { return c == Enum.class; } // 檢查兩個列舉型別資料是否相同 static boolean equalsEnum(Enum<?> o1, Enum<?> o2) { checkParams(o1.getClass() != o2.getClass(), "o1 和 o2 不同時為列舉型別"); return o1 == o2; } /** * 比較兩個物件是否相等,如果物件實現了 Comparable 介面,則使用 compareTo 方法進行比較 * 否則使用 Object 的 equals 方法進行物件的比較 * * @param o1 : 舊值資料物件 * @param o2 : 新值資料物件 */ @SuppressWarnings("unchecked") static boolean equalsObj(Object o1, Object o2) { if (o1 instanceof Comparable) return ((Comparable<Object>) o1).compareTo(o2) == 0; return o1.equals(o2); } /** * 判斷兩個集合(Collection)中的元素是否相同,這裡的實現只針對 Set 和 List <br /> * 對於 Set : 如果存在不同的元素,則直接將兩個集合作為比較物件儲存到 differMap 中 <br /> * 對於 List : 如果相同的索引位置的元素不同,那麼會記錄當前元素的索引位置的新舊值到 differMap, * 如果兩個列表的長度不一致,則會使用 null 來代替不存在的元素,對於元素的比較同樣基於 {@code dfs}<br /> * 對於陣列 : 首先將陣列轉換為對應的 {@code List},再使用 List 的比較方法進行比較 <br /> * * <br /> * <br /> * 對於其它的集合型別,將會丟擲一個 {@code RuntimeException} * <br /> * <br /> * * @param o1 : 舊集合資料物件 * @param o2 : 新集合資料物件 * @param prefix : 當前集合屬性所在的級別的字首字串表現形式 * @param differMap : 儲存不同屬性欄位的 Map */ static boolean equalsCollect( Object o1, Object o2, String prefix, Map<String, Node<Object>> differMap ) { Class<?> c1 = o1.getClass(), c2 = o2.getClass(); checkParams(c1 != c2, "集合 o1 和 o2 的型別不一致."); /* 對於集合來講,只能大致判斷一下兩個集合的元素是否是一致的, 這是由於集合本身不具備隨機訪問的特性,因此如果兩個集合存在不相等的元素, 那麼將會直接將兩個集合儲存的不同節點中 */ if (o1 instanceof Set) { // 分別計算兩個集合的資訊指紋 long h1 = 0, h2 = 0; long hash = BigInteger .probablePrime(32, current()) .longValue(); // 隨機的大質數用於隨機化資訊指紋 Set<?> s1 = (Set<?>) o1, s2 = (Set<?>) o2; for (Object obj : s1) h1 += genHash(obj) * hash; for (Object obj : s2) h2 += genHash(obj) * hash; if (h1 != h2) { differMap.put(prefix, new Node<>(s1, s2)); return false; } return true; } /* 對於列表來講,由於列表的元素存在順序, 因此可以針對不同的索引位置的元素進行對應的比較 */ if (o1 instanceof List) { List<?> list1 = (List<?>) o1, list2 = (List<?>) o2; return differList(list1, list2, prefix, differMap); } /* 對於陣列型別的處理,可以轉換為 List 進行類似的處理 */ if (c1.isArray()) { List<?> list1 = Arrays.stream((Object[]) o1).collect(Collectors.toList()); List<?> list2 = Arrays.stream((Object[]) o2).collect(Collectors.toList()); return differList(list1, list2, prefix, differMap); } log.debug("type={}", o1.getClass()); throw new RuntimeException("未能處理的集合型別異常"); } /** * 比較兩個 {@code List} 物件之間的不同元素 */ static boolean differList( List<?> list1, List<?> list2, String prefix, Map<String, Node<Object>> differMap ) { boolean res = true; Map<String, Node<Object>> tmpMap = new HashMap<>(); // 記錄相同索引位置的索引元素的不同 int i; for (i = 0; i < list1.size() && i < list2.size(); ++i) { res &= dfs(list1.get(i), list2.get(i), prefix + "#" + i, tmpMap); } differMap.putAll(tmpMap); // 新增到原有的不同 differMap 中 // 後續如果集合存在多餘的元素,那麼肯定這兩個位置的索引元素不同 while (i < list1.size()) { res = false; differMap.put(prefix + "#" + i, new Node<>(list1.get(i), null)); i++; } while (i < list2.size()) { res = false; differMap.put(prefix + "#" + i, new Node<>(null, list2.get(i))); i++; } // 後續元素處理結束 return res; } /** * 比較兩個 Map 屬性值,對於不相交的 key,使用 null 來代替現有的 key 值 * 當兩個 Map 中都存在相同的 key 是,將會使用遞迴處理 {@code dfs} 繼續比較 value 是否一致 * * @param m1 : 舊值屬性物件 Map 欄位 * @param m2 : 新值屬性物件的 Map 欄位 * @param prefix : 此時已經處理的物件的欄位深度 * @param differMap : 記錄不同的屬性值的 Map */ static boolean equalsMap( Map<?, ?> m1, Map<?, ?> m2, String prefix, Map<String, Node<Object>> differMap ) { checkParams(m1.getClass() != m2.getClass(), "map1 和 map2 型別不一致"); boolean res = true; // 首先比較兩個 Map 都存在的 key 對應的 value 物件 for (Object key : m1.keySet()) { String curPrefix = prefix + "." + key; if (!m2.containsKey(key)) { // 如果 m2 不包含 m1 的 key,此時是一個不同元素值 differMap.put(curPrefix, new Node<>(m1.get(key), null)); res = false; continue; } res &= dfs(m1.get(key), m2.get(key), curPrefix, differMap); } // 檢查 m1 中存在沒有 m2 的 key 的情況 for (Object key : m2.keySet()) { String curPrefix = prefix + "." + key; if (!m1.containsKey(key)) { differMap.put(curPrefix, new Node<>(null, m2.get(key))); res = false; } } return res; } private static void checkParams(boolean b, String s) { if (b) { throw new RuntimeException(s); } } /** * 獲取傳入的引數物件的 hash 值,具體的計算方式為: 遍歷當前物件的所有屬性欄位,將每個欄位視為一個 * {@code PRIME} 進位制數的一個數字,最終得到這個數將被視為當前物件的 hash 值 * <br /> * <br /> * 對於基本資料型別來講,將會將其強制轉換為 {@code long} 型別的整數參與計算 * <br /> * 而對於集合型別 {@code Collection} 和陣列型別來講,將會將整個集合整體視為一個欄位, * 將集合中所有元素按照相同的方式計算 hash 值,最後相加即為該集合的 hash 值 * <br /> * 對於其它已經自定義 hashCode 方法的物件(如 {@code BigInteger}、{@code Date} 等), * 將使用 {@code com.google.common.base.Objects} 的 hashCode 計算對應的 hash 值 * <br /> * 對於其它型別的屬性,由於不存在重寫的 hashcode 方法,這些屬性欄位將被視為獨立物件遞迴進行處理 * <br /> * * @param obj : 待計算 hashcode 的物件 * @return : 該物件生成的 hash 值 */ static long genHash(Object obj) { Class<?> c = obj.getClass(); long ans = 0L; // 能夠自行產生 hashcode 的型別 if (c.isPrimitive() || isBasicType(c) || isEnum(c) || isMap(c)) { return Objects.hashCode(obj); } if (c.isArray()) { // 針對陣列型別 Iterator<?> iterator = Arrays.stream(((Object[]) obj)).iterator(); while (iterator.hasNext()) ans = ans * PRIME + genHash(iterator.next()); return ans; } if (Collection.class.isAssignableFrom(c)) { // 針對集合型別 for (Object tmp : (Collection<?>) obj) ans = ans * PRIME + genHash(tmp); return ans; } Field[] fields = obj.getClass().getDeclaredFields(); for (Field field : fields) { int modifier = field.getModifiers(); if ((modifier & Modifier.STATIC) != 0) continue; try { field.setAccessible(true); Object tmp = field.get(obj); if (field.getType().isPrimitive()) { // 對於基本資料型別需要進行特殊的處理 ans = ans * PRIME + ((Number) tmp).longValue(); continue; } // 能夠使用 Objects 計算 hashCode 的類,需要進行單獨的處理 if (isBasicType(c) || isEnum(c) || isMap(c)) { ans = ans * PRIME + Objects.hashCode(tmp); continue; } // 對於其餘的情況,說明該屬性欄位是一個自定義物件,遞迴對每個欄位進行處理 ans = ans * PRIME + genHash(obj); } catch (IllegalAccessException e) { e.printStackTrace(); } } return ans; } }
需要新增以下的依賴項:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
對於前端來講,可以使用類似的方式來進行解析:
let diff = {};
let obj = {};
const listRegex = new RegExp("[^#]+#(\\d+)");
let diffKeys = Object.keys(diff);
for (let i = 0; i < diffKeys.length; ++i) {
let key = diffKeys[i];
let fs = key.split(".");
dfs(obj, fs, 0, diff[key]);
}
console.log(obj);
function dfs(curObj, fs, idx, diffVal) {
if (idx >= fs.length) return;
if (idx === fs.length - 1) {
curObj[fs[idx]] = diffVal;
return;
}
if (listRegex.test(fs[idx])) {
let arr = fs[idx].split("#");
curObj = curObj[arr[0]][arr[1]];
} else {
curObj = curObj[fs[idx]];
}
dfs(curObj, fs, idx + 1, diffVal);
}