两个Long类型真的不能直接用>或<比较么?其实可以

当我在Google输入“Long类型的比较”时,会出现多如牛毛的与这个问题相关的博文,并且这些博文对此问题的看法如出一辙,都“不约而同”地持有如下观点:

对于Long类型的数据,它是一个对象,所以对象不可以直接通过“>”,“==”,“<”的比较。若要比较是否相等,可以用Long对象的equals方法;若要进行“>”,“<”的比较,需通过Long对象的longValue方法。

那么问题来了,这个观点真的全对吗?或者准确地说,后半段关于“>”,“<”的说法真的对吗?起初我也差点信了,按理说Java中并没有像C++中的操作符重载之类的东东,对象直接拿来用“>”或“<”比较确实很少这么干的,而且有童鞋可能会说,既然大家都这么说,当然是对的无疑咯。那么今天笔者想告诉你的是,它是错的Long类型可以直接用“>”和“<”比较,并且其他包装类型也同理。不信?先别急着反驳,且听笔者娓娓道来。

问题起源

关于Long类型的大小比较这个问题,其实是源于我的上一篇博文谈谈ali与Google的Java开发规范,在其中关于“相同类型的包装类对象之间值的比较”这一规范,我补充了如下一点:Image1.png

然后oschina上的一个热心网友关于此提出了一个很好的问题:Image2.png

即有没有可能比较的是内存地址并且刚好其大小满足上述条件?想想也不无道理,毕竟对于Java中的对象引用a、b、c的值实际就是对象在堆中的地址。关于这个问题,其实我最初也质疑过,为此我编写了多种类似上面的testCase,比如:

Long a = new Long(1000L);
Long b = new Long(2000L);
Long c = new Long(222L);
Assert.isTrue(a<b && a>c);	//断言成功

最终的结论跟预期一致的:两者的比较结果跟Long对象中的数值大小的比较结果是一致的,至少从目前所尝试过的所有testCase来看是这样的。

从现象到本质

但是,光靠那几个有限的单元测试,貌似并不具有较强的说服力,心中难免总有疑惑:会不会有特殊的case没覆盖到?会不会还是地址比较的巧合?怎么才能有效地验证我的结论呢?

于是我开始琢磨:毕竟对于new Long()这种操作,是在堆中动态分配内存的,我们不太好控制a、b等的地址大小,那又该怎么验证上述的比较不是地址比较的结果呢?除了地址之外,还有别的我们能控制的吗?有的,那就是对象中的内容!我们可以在不改变对象引用值的情况下,改变对象的内容,然后看其比较结果是否发生变化,这对于我们来说轻而易举。有时候换个角度思考问题,就能有新的收获!

一、debug验证

那么接下来,我们就可以用反证法来证明上述问题,还是以本文开头的testCase为例:假设上述testCase中比较的是地址值,只要我们不对a、b进行赋值操作,即不改变它们的地址值,其比较结果就应该也是始终不变,此时我们仅修改对象中的数值,这里对应Long对象中的value字段,使数值的大小比较与当前Long对象的比较结果相反,如果此时Long对象的比较结果也跟着变为相反,也就推翻了地址比较这一假设,否则就是地址比较,证毕。

接下来以实例来演示我们的推断过程。首先上代码:

/**
 * @author sherlockyb
 * @2018年1月14日
 */
<think> 我们有两个 List<Map<String, String>>,需要比较它们的内容,找出不匹配的数据(即在一个列表中存在而在另一个中不存在,者相同key的value不同的情况),并打印日志。 注意:这里比较的是每个Map的值,而不是比较引用。而且,两个列表中的Map可能并不是按照相同的顺序排列。 思路: 1. 我们可以将两个列表都转换为某种易于比较的形式,例如转换为Set<String>(每个Map转换为一个字符串表示)?但是这样会丢失结构信息,且无法知道具体哪个字段不同。 2. 另一种方法是逐个比较每个Map。由于两个列表可能顺序不一致,所以不能直接按索引比较。我们可以为每个Map定义一个唯一的键(如果有的话),者按照多个键组合来标识一个记录。 但是,问题中并没有提到Map中有可以作为唯一标识的键。所以我们需要假设没有唯一标识,者用户需要完全匹配整个Map。 3. 因此,我们考虑以下两种不匹配的情况: a) 在list1中存在,而在list2中找不到完全相同的Map(即所有键值对完全相同)。 b) 在list2中存在,而在list1中找不到完全相同的Map。 4. 注意:如果两个列表中有多个相同的Map,那么应该考虑出现次数。即如果一个Map在list1中出现2次,在list2中出现1次,则认为有一次不匹配(具体看需求:是按集合还是按多重集合?) 但通常,我们可能需要考虑位置者重复元素?这里问题没有明确,但根据需求“比较值”,我们可能需要考虑重复元素,即两个列表应该包含完全相同的Map(包括重复次数)?还是不考虑重复? 5. 用户需求是“比较两个List<Map<String, String>>类型数据的值”,这里我们通常理解为两个集合(不考虑顺序)的比较,但是要考虑重复元素吗?由于List是有序且可重复的,但用户可能关心的是数据内容而不关心顺序和重复次数? 问题没有明确,所以我们可以提供两种方式: 方式一:不考虑顺序,但考虑重复次数(即两个列表当多重集合来比较,每个Map出现的次数要相同)。 方式二:不考虑顺序也不考虑重复次数(即两个列表作为集合来比较,重复的相同Map只算一次)。 6. 但是,打印日志需要具体到哪些数据不匹配,所以我们需要找出两个列表的差异。 7. 我们可以借助Apache Commons Collections者Guava库,但这里我们使用纯Java实现。 8. 由于Map没有实现Comparable,所以我们可以将Map转换为字符串(比如JSON),者使用自定义的Comparator来比较Map。但是注意,两个Map的键值对顺序可能不同,但内容相同,所以比较时应该考虑这一点。 9. 步骤: a) 为了比较两个Map是否相等,我们可以使用Map的equals方法:因为Map的equals方法已经实现为比较键和值是否相等,而不关心顺序(因为HashMap的equals实现就是比较键值对的集合)。 b) 但是,由于我们存储的是List<Map>,所以我们需要比较每个Map是否相等。 10. 方法选择: 我们可以使用两个List的containsAll方法?但是这样不能处理重复元素。例如: list1: [mapA, mapA] list2: [mapA] 那么list1.containsAll(list2)返回true,但list2.containsAll(list1)返回false。这样我们可以知道有差异,但不能准确知道差异是什么。 11. 更好的方法是:分别创建两个列表的副本,然后逐个移除相同的元素。最终剩余的元素就是不匹配的元素。 步骤: - 创建两个新的ArrayList(作为副本),分别命名为list1Copy和list2Copy。 - 遍历list1Copy,对于每个元素,尝试在list2Copy中查找并移除一个相同的元素(使用equals方法判断)。 - 如果找到了,从两个副本中都移除这个元素(注意:只移除一个匹配项,因为可能有重复)。 - 最后,两个副本中剩余的元素就是不匹配的元素。 12. 但是,这样的效率是O(n^2),因为对于list1Copy中的每个元素,都需要遍历list2Copy来查找。如果数据量不大,可以接受;如果数据量大,可以考虑使用计数的方式(如HashMap来计数每个Map出现的次数)。 13. 如何计数?由于Map是对象,我们可以用Map本身作为HashMap的键吗?可以,但是要注意:如果两个Map内容相同但引用不同,那么作为键是会相等的(因为HashMap调用equals和hashCode)。但是,我们使用的Map一般是HashMap,而HashMap的equals已经重写,所以只要内容相同,equals返回true,hashCode也相同,因此可以作为键。 14. 步骤(使用计数): Map<Map<String, String>, Integer> countMap = new HashMap<>(); a) 遍历第一个列表,对于每个Map,countMap中增加计数。 b) 遍历第二个列表,对于每个Map,countMap中减少计数。 c) 然后,countMap中计数不为0的Map就是有差异的。但是,这样我们不知道是哪个列表多出来的。 15. 但是我们如何记录具体是哪个列表多出来的?计数为正表示第一个列表多,为负表示第二个列表多。但是,这样我们只能知道数量差异,不能区分具体的Map实例(因为相同的Map会被合并计数)。 16. 打印日志需要具体到每个不匹配的Map,所以我们希望输出: - 在第一个列表中存在但第二个列表中不存在的Map(可能是整个Map不存在,者存在但次数不够)。 - 在第二个列表中存在但第一个列表中不存在的Map。 17. 因此,我们可以这样: List<Map<String, String>> onlyInList1 = new ArrayList<>(); List<Map<String, String>> onlyInList2 = new ArrayList<>(); 第一步:将list1的所有元素加入一个计数Map(键为Map,值为出现次数)。 第二步:遍历list2,对于每个元素: 如果计数Map中存在该键,且计数>0,则计数减1; 否则,将这个元素添加到onlyInList2(表示在list2中存在但在list1中已经匹配完了,所以这个是多出来的)。 第三步:遍历计数Map,将计数>0的键(根据次数重复添加)添加到onlyInList1(表示在list1中多出来的)。 18. 但是,这样我们需要把计数Map中剩余的元素展开成列表(重复多少次就添加多少次)。 19. 另一种实现: Map<Map<String, String>, Integer> countMap = new HashMap<>(); for (Map<String, String> map : list1) { countMap.put(map, countMap.getOrDefault(map, 0) + 1); } for (Map<String, String> map : list2) { // 如果当前map在countMap中存在且计数>0,则计数减1;否则,这个map在list2中多出来了 if (countMap.containsKey(map) && countMap.get(map) > 0) { countMap.put(map, countMap.get(map) - 1); } else { onlyInList2.add(map); } } // 然后,遍历countMap,将剩余计数对应的map展开(重复次数)放入onlyInList1 for (Map.Entry<Map<String, String>, Integer> entry : countMap.entrySet()) { for (int i = 0; i < entry.getValue(); i++) { onlyInList1.add(entry.getKey()); } } 20. 但是,这里有一个问题:两个Map内容相同但引用不同,但是作为键是相等的,所以没问题(因为HashMap比较的是内容)。 21. 但是,注意:如果两个Map内容相同,但是它们的hashCode计算方式不同(比如我们自己实现的Map类)?这里我们默认使用的是HashMap,所以hashCode和equals都是基于内容的。 22. 然而,这里有一个陷阱:我们使用Map作为另一个Map的键,而Map是可变的。一旦作为键后修改了内容,就会破坏HashMap的结构。但是在这个方法中,我们只是临时使用,而且不会修改这些Map,所以安全。 23. 但是,如果两个Map的内容相同,但顺序不同(比如一个Map先放key1后放key2,另一个先放key2后放key1),那么作为HashMap的键,它们是相等的吗? HashMap的equals方法:比较两个Map的entrySet是否相等。而entrySet的比较与顺序无关,只要包含相同的键值对就相等。所以,两个内容相同但添加顺序不同的Map是相等的。 24. 因此,这个方法可行。 25. 但是,如果数据量很大,而且Map的键很多,那么计算hashCode可能会比较耗时。另外,这种方式要求Map中的键值对都是可比较的(String的equals和hashCode没问题)。 26. 如果用户希望知道具体哪个字段不同,那么上述方法只能知道整个Map不同,不能细化到字段。所以我们需要另一种方法:逐条对比,并找出具体的差异。 但是,这个问题只要求打印不匹配的数据(整个Map),并没有要求字段级别的差异。所以我们可以先按整个Map比较。 27. 实现步骤: a) 创建两个列表的副本,为了避免修改原数据。 b) 使用上述计数方法来找出两个列表中差异的元素(即onlyInList1和onlyInList2)。 c) 然后打印日志:输出两个列表的差异。 28. 代码示例: import java.util.*; import java.util.stream.Collectors; public class CompareListMap { public static void main(String[] args) { // 示例数据 List<Map<String, String>> list1 = new ArrayList<>(); List<Map<String, String>> list2 = new ArrayList<>(); // 填充数据... // 比较两个列表 List<Map<String, String>> onlyInList1 = new ArrayList<>(); List<Map<String, String>> onlyInList2 = new ArrayList<>(); // 使用计数方法 Map<Map<String, String>, Integer> countMap = new HashMap<>(); // 遍历第一个列表 for (Map<String, String> map : list1) { // 这里为了安全,我们使用一个新的HashMap来包装,避免后续修改?但原列表的Map我们不会修改,所以可以直接使用。 countMap.put(map, countMap.getOrDefault(map, 0) + 1); } // 遍历第二个列表 for (Map<String, String> map : list2) { if (countMap.containsKey(map)) { int count = countMap.get(map); if (count > 0) { countMap.put(map, count - 1); } else { // 计数已经为0,说明list2中出现了该Map的次数多于list1 onlyInList2.add(map); } } else { // 在countMap中不存在 onlyInList2.add(map); } } // 遍历countMap,将剩余在list1中但未在list2中匹配的Map添加到onlyInList1 for (Map.Entry<Map<String, String>, Integer> entry : countMap.entrySet()) { Map<String, String> map = entry.getKey(); int count = entry.getValue(); for (int i = 0; i < count; i++) { onlyInList1.add(map); } } // 打印日志: if (onlyInList1.isEmpty() && onlyInList2.isEmpty()) { System.out.println("两个列表完全相同"); } else { if (!onlyInList1.isEmpty()) { System.out.println("在第一个列表中但不在第二个列表中的元素:"); for (Map<String, String> map : onlyInList1) { System.out.println(map); } } if (!onlyInList2.isEmpty()) { System.out.println("在第二个列表中但不在第一个列表中的元素:"); for (Map<String, String> map : onlyInList2) { System.out.println(map); } } } } } 29. 但是,上述代码有一个问题:在第二个列表遍历时,对于countMap中存在的键,但计数为0的情况,我们将其添加到onlyInList2。但实际上,这种情况不应该发生,因为countMap中计数为0的键在countMap中虽然存在,但已经匹配完了。所以这时候这个map在list2中多出来了一次。 所以我们上面的处理是合理的。 30. 另外,还有一种情况:countMap中存在该键,但计数为0,那么我们会将其加入onlyInList2,然后继续。同时,countMap中该键的计数不变(还是0),后面在输出剩余计数时不会输出(因为计数为0不会添加)。 31. 注意:上面的计数方法要求Map的hashCode和equals正确实现。如果用户使用的是自定义的Map实现(比如自己写的类,没有重写equals和hashCode),那么可能出错。但这里我们假设是标准的HashMap。 32. 如果用户想要忽略重复元素(即把两个列表当作Set来比较),那么我们可以先对每个列表去重,然后再比较。但用户需求是List,所以默认应该考虑重复。 33. 如果用户希望按照某个字段来标识一条记录(比如"id"字段),那么我们可以重新设计:将两个列表转换为以id为键的Map,然后比较相同id的Map内容是否相同。这样就能知道具体哪个字段不同。但是,用户没有提供唯一标识字段,所以无法按此处理。 34. 因此,我们按照整个Map内容比较。 35. 优化:可以使用Java 8的Stream API来实现计数,但为了清晰,我们使用上述循环。 36. 另外,如果两个列表很大,那么使用Map计数的方法在空间上可能会有较大开销(因为每个不同的Map都会存储一次)。 37. 打印日志:我们使用System.out.println,实际中可以使用日志框架(如log4j)来打印。 38. 考虑到用户可能希望将不匹配的数据记录到日志文件,我们可以将差异数据以更友好的方式输出(比如JSON格式),但这里简单输出Map的toString。 39. 最后,如果用户需要字段级别的比较,那么可能需要更复杂的实现(例如,每个Map按照相同的键进行比较,并忽略一些键),但这里用户没有要求。 40. 根据引用内容,用户之前可能在做List<Map>的复制者转换,所以这里我们提供整个Map比较的通用方法。 41. 代码实现注意:我们直接使用了原Map对象作为键,所以不会修改原Map。但是,如果原Map在后续被修改,那么就可能出现问题(因为作为键的Map被修改后,其hashCode会变)。但在这个方法中,countMap是局部变量,方法结束后就释放了,所以不影响。 42. 另外,如果两个列表非常大,我们可以优化:将每个Map计算一个摘要(比如MD5),然后用摘要作为计数Map的键,这样可以减少内存占用(因为Map对象被摘要替代)。但是,摘要可能冲突,所以需要权衡。这里我们假设数据量不大。 43. 最终,我们给出上述代码。 44. 测试:用用户给出的引用[1]中的示例数据来测试?但引用[1]中的代码并不是比较,而是复制,所以我们可以自己构造简单数据测试。 45. 构造测试数据: list1: map1: {"id":"1", "name":"A"} map2: {"id":"2", "name":"B"} map3: {"id":"1", "name":"A"} // 重复的map1 list2: map1: {"id":"1", "name":"A"} map4: {"id":"3", "name":"C"} 期望: onlyInList1: map2, map3(因为map1匹配了一次,但list1中还有map2和另一个map1(重复)没有被匹配,但实际上map3应该匹配list2中的map1?不对,应该是: 在比较时,list1有三个元素,list2有两个元素。 遍历list2:第一个map1在countMap中,计数减去1(变成1);第二个map4不在countMap中,加入onlyInList2。 然后,countMap中剩余:map1:1, map2:1, map3:1?不对,因为我们计数时是将重复的map1计数为2(因为出现了两次)。 然后遍历list2:匹配到map1一次,计数减1,剩余1次。 然后,countMap中剩余:map1:1, map2:1, map3:1?不对,实际上在countMap中,map1出现2次,map2和map3各1次,但map3其实和map1相同吗? 注意:这里的map3和map1内容相同,所以countMap中键map1的计数是2。然后list2中有一个map1,所以计数减1变成1。然后countMap中剩余:map1:1, map2:1(注意map3就是map1,所以已经合并了)。 所以最后onlyInList1中应该有两个元素:map1(剩余1次)和map2(1次)。但是map3被合并了,所以我们就添加两次map1?不对,我们添加的是键对应的Map,添加次数为计数。 所以onlyInList1会有两个元素:第一次添加map1(因为计数1),第二次添加map2(计数1)。但是map3呢?因为map3和map1相同,所以已经被合并了。 所以输出: onlyInList1: [{"id":"1","name":"A"}, {"id":"2","name":"B"}] onlyInList2: [{"id":"3","name":"C"}] 而list1中原来的map3(也就是第二个map1)并没有单独出现,因为它和map1相同,所以被合并计数了。 所以,这个方法能够正确合并相同的Map,并统计次数。 46. 因此,我们按照这个思路写代码。 47. 考虑到用户可能希望按照原始顺序输出差异(但问题没有要求),我们计数方法会打乱顺序。所以只能输出所有差异,但顺序不定。 48. 如果用户需要原始顺序,那么我们可以使用第一种方法(遍历移除),但效率低。 49. 我们提供的方法适用于不考虑顺序,但考虑重复的情况。 50. 现在,我们根据上述思路编写比较方法,并打印日志。 </think> 在 Java 中比较两个 `List<Map<String, String>>` 的值并打印不匹配数据,可以通过以下步骤实现: ### 解决方案 ```java import java.util.*; import java.util.stream.Collectors; public class ListMapComparator { public static void compareAndLog( List<Map<String, String>> list1, List<Map<String, String>> list2 ) { // 1. 将 List 转换为 Set<Map>(忽略顺序,考虑内容) Set<Map<String, String>> set1 = list1.stream() .map(HashMap::new) // 创建新 Map 避免引用干扰 .collect(Collectors.toSet()); Set<Map<String, String>> set2 = list2.stream() .map(HashMap::new) .collect(Collectors.toSet()); // 2. 找出不匹配的数据 List<Map<String, String>> onlyInList1 = list1.stream() .filter(map -> !set2.contains(new HashMap<>(map))) .collect(Collectors.toList()); List<Map<String, String>> onlyInList2 = list2.stream() .filter(map -> !set1.contains(new HashMap<>(map))) .collect(Collectors.toList()); // 3. 打印日志 if (onlyInList1.isEmpty() && onlyInList2.isEmpty()) { System.out.println("INFO: 两个列表完全相同"); } else { if (!onlyInList1.isEmpty()) { System.out.println("WARN: 仅在第一个列表中的数据:"); onlyInList1.forEach(map -> System.out.println("\t" + formatMap(map)) ); } if (!onlyInList2.isEmpty()) { System.out.println("WARN: 仅在第二个列表中的数据:"); onlyInList2.forEach(map -> System.out.println("\t" + formatMap(map)) ); } } } // 格式化 Map 输出为可读字符串 private static String formatMap(Map<String, String> map) { return map.entrySet().stream() .map(entry -> entry.getKey() + "=" + entry.getValue()) .collect(Collectors.joining(", ", "{", "}")); } public static void main(String[] args) { // 测试数据 List<Map<String, String>> list1 = new ArrayList<>(); list1.add(Map.of("id", "1", "name", "Alice")); list1.add(Map.of("id", "2", "name", "Bob")); List<Map<String, String>> list2 = new ArrayList<>(); list2.add(Map.of("id", "1", "name", "Alice")); list2.add(Map.of("id", "3", "name", "Charlie")); // 执行比较 compareAndLog(list1, list2); } } ``` ### 关键说明 1. **内容比较而非引用** - 使用 `new HashMap<>(map)` 创建新对象,避免因引用相同导致的误判 - `Map.equals()` 已实现内容比较(要求键值对完全一致) 2. **忽略顺序但考虑重复** - 通过 `Set` 转换实现顺序无关性 - 若需考虑重复数据,改用 *多重集合* 计数: ```java // 替代方案:统计出现次数(考虑重复) Map<Map<String, String>, Long> countMap1 = list1.stream() .collect(Collectors.groupingBy(HashMap::new, Collectors.counting())); ``` 3. **日志输出优化** - 格式化输出 `{key1=value1, key2=value2}` 增强可读性 - 区分 `INFO`/`WARN` 级别日志便于排查 4. **性能注意事项** - 时间复杂度:$O(n+m)$ (n, m 为两个列表长度) - 空间复杂度:$O(n+m)$ (需额外存储 Set) ### 处理结果示例 ``` WARN: 仅在第一个列表中的数据: {id=2, name=Bob} WARN: 仅在第二个列表中的数据: {id=3, name=Charlie} ``` ### 应用场景 此方案适用于: 1. 数据迁移校验 2. API 响应结果比对 3. 数据库快照对比 4. 单元测试中预期值与实际值的断言[^2][^3]
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值