diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java index fe03757c6..0fc4adbf8 100755 --- a/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java @@ -395,22 +395,57 @@ public class CollUtil { * @since 5.3.5 */ public static List subtractToList(Collection coll1, Collection coll2) { + return subtractToList(coll1, coll2, true); + } + /** + * 计算集合的单差集,即只返回【集合1】中有,但是【集合2】中没有的元素 + * 只要【集合1】中的某个元素在【集合2】中存在(equals和hashcode),就会被排除 + * + *
+	 * 示例:
+	 * 1. subtractToList([null, null, null, null], [null, null]) → []
+	 * 2. subtractToList([null, null, null, null], [null, null, "c"]) → []
+	 * 3. subtractToList(["a", "b", "c"], ["a", "b", "c"]) → []
+	 * 4. subtractToList([], ["a", "b", "c"]) → []
+	 * 5. subtractToList(["a", "b", "c"], []) → ["a", "b", "c"]
+	 * 6. subtractToList(["a", "a", "b", "b", "c", "c", "d"], ["b", "c"]) → ["a", "a", "d"]
+	 * 7. subtractToList(["a", null, "b"], ["a", "c"]) → [null, "b"]
+	 * 8. subtractToList(["a", "b", "c"], ["d", "e", "f"]) → ["a", "b", "c"]
+	 * 9. subtractToList(["a", "a", "b", "b", "c"], ["d", "e", "f"]) → ["a", "a", "b", "b", "c"]
+	 * 
+ * + * @param coll1 集合1,需要计算差集的源集合 + * @param coll2 集合2,需要从集合1中排除的元素所在集合 + * @param isLinked 返回的集合类型是否是LinkedList,{@code true}返回{@link LinkedList},{@code false}返回{@link ArrayList} + * @param 元素类型 + * @return 单差集结果。当【集合1】为空时返回空列表;当【集合2】为空时返回集合1的拷贝;否则返回【集合1】中排除【集合2】中所有元素后的结果 + */ + public static List subtractToList(Collection coll1, Collection coll2, boolean isLinked) { if (isEmpty(coll1)) { return ListUtil.empty(); } + if (isEmpty(coll2)) { - return ListUtil.list(true, coll1); + return ListUtil.list(isLinked, coll1); } - //将被交数用链表储存,防止因为频繁扩容影响性能 - final List result = new LinkedList<>(); + /* + 返回的集合最大不会超过 coll1.size + 所以这里创建 ArrayList 时 initialCapacity 给的是 coll1.size + 这样做可以避免频繁扩容 + */ + final List result = isLinked + ? new LinkedList<>() + : new ArrayList<>(coll1.size()); + Set set = new HashSet<>(coll2); for (T t : coll1) { - if (false == set.contains(t)) { + if (!set.contains(t)) { result.add(t); } } + return result; } diff --git a/hutool-core/src/test/java/cn/hutool/core/collection/CollUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/collection/CollUtilTest.java index de32a7829..5615a97aa 100755 --- a/hutool-core/src/test/java/cn/hutool/core/collection/CollUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/collection/CollUtilTest.java @@ -845,6 +845,361 @@ public class CollUtilTest { assertEquals(1L, (long) result.get(0)); } + @Test + public void subtractToListAllNullTest() { + final List list1 = new ArrayList<>(); + list1.add(null); + list1.add(null); + list1.add(null); + list1.add(null); + + final List list2 = new ArrayList<>(); + list2.add(null); + list2.add(null); + + final List result1 = CollUtil.subtractToList(list1, list2); + assertTrue(result1.isEmpty()); + assertNotSame(result1, list1); + + list2.add("c"); + final List result2 = CollUtil.subtractToList(list1, list2); + assertTrue(result2.isEmpty()); + assertNotSame(result2, list1); + } + + @Test + public void subtractToListEmptyTest() { + // 测试第一个集合为空的情况 + final List list1 = Collections.emptyList(); + final List list2 = Arrays.asList("a", "b", "c"); + + // 第一个集合为空时应返回空列表 + final List result1 = CollUtil.subtractToList(list1, list2); + assertTrue(result1.isEmpty()); + + // 测试第二个集合为空的情况 + final List list3 = Arrays.asList("a", "b", "c"); + final List list4 = Collections.emptyList(); + + // 第二个集合为空时应返回第一个集合的拷贝 + final List result2 = CollUtil.subtractToList(list3, list4); + assertEquals(3, result2.size()); + assertEquals(list3, result2); + assertNotSame(list3, result2); + } + + @Test + public void subtractToListDuplicateTest() { + // 测试第一个集合中有重复元素的情况 + final List list1 = Arrays.asList("a", "a", "b", "b", "c", "c", "d"); + final List list2 = Arrays.asList("b", "c"); + + // 应该返回所有不在第二个集合中的元素,包括重复的 + final List result = CollUtil.subtractToList(list1, list2); + assertEquals(3, result.size()); + assertEquals("a", result.get(0)); + assertEquals("a", result.get(1)); + assertEquals("d", result.get(2)); + } + + @Test + public void subtractToListNoCommonElementsTest() { + // 测试集合1和集合2不包含相同元素的情况 + final List list1 = Arrays.asList("a", "b", "c"); + final List list2 = Arrays.asList("d", "e", "f"); + + // 期望结果:返回集合1的完整拷贝 + final List result = CollUtil.subtractToList(list1, list2); + assertEquals(3, result.size()); + assertEquals("a", result.get(0)); + assertEquals("b", result.get(1)); + assertEquals("c", result.get(2)); + assertEquals(list1, result); + assertNotSame(list1, result); // 确保返回的是拷贝而不是原始引用 + + // 测试集合1中有重复元素的情况 + final List list3 = Arrays.asList("a", "a", "b", "b", "c"); + final List list4 = Arrays.asList("d", "e", "f"); + + // 期望结果:返回集合1的完整拷贝,包括重复元素 + final List result2 = CollUtil.subtractToList(list3, list4); + assertEquals(5, result2.size()); + assertEquals("a", result2.get(0)); + assertEquals("a", result2.get(1)); + assertEquals("b", result2.get(2)); + assertEquals("b", result2.get(3)); + assertEquals("c", result2.get(4)); + assertEquals(list3, result2); + assertNotSame(list3, result2); + + // 测试不同类型的元素但确保两个集合的泛型类型一致 + final List list5 = Arrays.asList(1, 2, 3); + final List list6 = Arrays.asList(4, 5, 6); + + // 期望结果:返回集合1的完整拷贝 + final List result3 = CollUtil.subtractToList(list5, list6); + assertEquals(3, result3.size()); + assertEquals(Integer.valueOf(1), result3.get(0)); + assertEquals(Integer.valueOf(2), result3.get(1)); + assertEquals(Integer.valueOf(3), result3.get(2)); + assertEquals(list5, result3); + assertNotSame(list5, result3); + } + + @Test + public void subtractToListWithLinkedTest() { + // 测试指定返回LinkedList的情况 + final List list1 = Arrays.asList(1, 2, 3, 4, 5); + final List list2 = Arrays.asList(2, 4); + + // 使用LinkedList + final List result1 = CollUtil.subtractToList(list1, list2, true); + assertInstanceOf(LinkedList.class, result1); + assertEquals(3, result1.size()); + assertEquals(Arrays.asList(1, 3, 5), result1); + + // 使用ArrayList + final List result2 = CollUtil.subtractToList(list1, list2, false); + assertInstanceOf(ArrayList.class, result2); + assertEquals(3, result2.size()); + assertEquals(Arrays.asList(1, 3, 5), result2); + } + + @Test + public void subtractToListWithNullElementsTest() { + // 测试包含null元素的情况 + final List list1 = new ArrayList<>(); + list1.add("a"); + list1.add(null); + list1.add("b"); + + final List list2 = Arrays.asList("a", "c"); + + // null元素不在list2中,应该保留 + final List result = CollUtil.subtractToList(list1, list2); + assertEquals(2, result.size()); + assertNull(result.get(0)); + assertEquals("b", result.get(1)); + } + + @Test + public void subtractToListLargeCollectionTest() { + // 测试大集合性能 + final int size = 10000; + final List list1 = new ArrayList<>(size); + final List list2 = new ArrayList<>(size / 2); + + // 构建测试数据,list1 size为 10000,元素为 [0, 9999] + for (int i = 0; i < size; i++) { + list1.add(i); + } + + // list2 size 为 5000,元素为 0, 2, 4, 6, 8, ..., 9996, 9998 + for (int i = 0; i < size / 2; i++) { + list2.add(i * 2); // 偶数 + } + + // 记录开始时间 + long startTime = System.currentTimeMillis(); + + // 执行操作 + final List result = CollUtil.subtractToList(list1, list2); + + // 记录结束时间 + long endTime = System.currentTimeMillis(); + + // 验证结果 - 应该只包含奇数 + assertEquals(size / 2, result.size()); + for (Integer num : result) { + assertEquals(1, num % 2); + } + + // 输出性能指标 + System.out.println("Large collection subtractToList took: " + (endTime - startTime) + "ms for " + size + " elements"); + } + + @Test + public void subtractToListPerformanceComparisonTest() { + // 比较不同实现方式的性能 + final int list1Size = 100000, list2Size = 1000; + final List list1 = new ArrayList<>(list1Size); + final List list2 = new ArrayList<>(list2Size); + + // 构建测试数据,list1 size 为 100000 + // 元素为 [0, 99999] + for (int i = 0; i < list1Size; i++) { + list1.add(i); + } + + // [0, 9999] + for (int i = 0; i < list2Size; i++) { + list2.add(i); + } + + // 测试LinkedList性能 + long startTime1 = System.currentTimeMillis(); + final List result1 = CollUtil.subtractToList(list1, list2, true); + long endTime1 = System.currentTimeMillis(); + long linkedListTime = endTime1 - startTime1; + + // 测试ArrayList性能 + long startTime2 = System.currentTimeMillis(); + final List result2 = CollUtil.subtractToList(list1, list2, false); + long endTime2 = System.currentTimeMillis(); + long arrayListTime = endTime2 - startTime2; + + // 验证结果相同 + assertEquals(result1.size(), result2.size()); + assertEquals(list1Size - list2Size, result1.size()); + + // 输出性能比较 + System.out.println("subtractToList performance comparison for " + list1Size + " elements:"); + System.out.println("LinkedList implementation: " + linkedListTime + "ms"); + System.out.println("ArrayList implementation: " + arrayListTime + "ms"); + } + + @Test + public void subtractToListPreservesOrderTest() { + // 测试确保保留原始集合的顺序 + final List list1 = Arrays.asList("c", "a", "d", "b", "e"); + final List list2 = Arrays.asList("a", "e"); + + // 减去后应该保持原顺序:c, d, b + final List result = CollUtil.subtractToList(list1, list2); + assertEquals(3, result.size()); + assertEquals("c", result.get(0)); + assertEquals("d", result.get(1)); + assertEquals("b", result.get(2)); + } + + @Test + public void subtractToListTypeTest() { + // 测试默认返回LinkedList的特性(旧版本特性) + final List list1 = Arrays.asList(1, 2, 3, 4, 5); + final List list2 = Arrays.asList(2, 4); + + // 不指定类型时,旧版本默认使用LinkedList + final List result = CollUtil.subtractToList(list1, list2); + assertInstanceOf(LinkedList.class, result); + assertEquals(3, result.size()); + assertEquals(Arrays.asList(1, 3, 5), result); + } + + @Test + public void subtractToListNullElementsComparisonTest() { + // 测试对null元素处理的一致性 + final ArrayList list1 = new ArrayList<>(); + list1.add(null); + list1.add("a"); + list1.add(null); + list1.add("b"); + + final ArrayList list2 = new ArrayList<>(); + list2.add("a"); + + // 默认调用(旧版行为) + final List result1 = CollUtil.subtractToList(list1, list2); + // 指定LinkedList(模拟旧版) + final List result2 = CollUtil.subtractToList(list1, list2, true); + // 指定ArrayList(新版特性) + final List result3 = CollUtil.subtractToList(list1, list2, false); + + // 验证三种结果一致, 都应该是 [null, null, "b"] + assertEquals(3, result1.size()); + assertEquals(3, result2.size()); + assertEquals(3, result3.size()); + + // 都应该包含null元素和 "b" + assertTrue(result1.contains(null)); + assertTrue(result1.contains("b")); + assertFalse(result1.contains("a")); + + // 旧版实现和新版保持空元素顺序一致性 + assertNull(result1.get(0)); + assertNull(result1.get(1)); + assertEquals("b", result1.get(2)); + + // 结果应该相同 + assertEquals(result1, result2); + assertEquals(result1, result3); + assertEquals(result1.toString(), result3.toString()); + } + + @Test + public void subtractToListWithCustomObjectsTest() { + // 测试自定义对象的情况 + final Person p1 = new Person("张三", 20, "male", 1); + final Person p2 = new Person("李四", 21, "female", 2); + final Person p3 = new Person("王五", 22, "male", 3); + final Person p4 = new Person("赵六", 23, "female", 4); + + final List list1 = Arrays.asList(p1, p2, p3, p4); + final List list2 = Arrays.asList(p2, p4); + + // 减去后应该只剩下p1和p3 + final List result = CollUtil.subtractToList(list1, list2); + assertEquals(2, result.size()); + assertEquals("张三", result.get(0).getName()); + assertEquals(20, result.get(0).getAge()); + assertEquals("male", result.get(0).getGender()); + assertEquals(1, result.get(0).getId()); + + assertEquals("王五", result.get(1).getName()); + assertEquals(22, result.get(1).getAge()); + assertEquals("male", result.get(1).getGender()); + assertEquals(3, result.get(1).getId()); + } + + @Test + public void subtractToListSameObjectsTest() { + // 测试两个集合有完全相同对象的情况 + final List list1 = Arrays.asList("a", "b", "c"); + final List list2 = Arrays.asList("a", "b", "c"); + + // 减去后应该为空列表 + final List result = CollUtil.subtractToList(list1, list2); + assertTrue(result.isEmpty()); + + // 验证结果类型 + assertInstanceOf(LinkedList.class, result); + assertNotSame(result, list1); + } + + @Test + public void subtractToListCollectionImplementationTest() { + // 测试非List集合实现的情况 + final Set set1 = new HashSet<>(Arrays.asList("a", "b", "c", "d")); + final Set set2 = new LinkedHashSet<>(Arrays.asList("b", "d")); + + // 减去后应该只剩下a和c + final List result = CollUtil.subtractToList(set1, set2); + assertEquals(2, result.size()); + assertTrue(result.contains("a")); + assertTrue(result.contains("c")); + + // 验证结果类型 + assertInstanceOf(LinkedList.class, result); + } + + @Test + public void subtractToListConsistencyTest() { + // 测试subtractToList与subtract方法的一致性 + final List list1 = Arrays.asList("a", "b", "c", "d", "e"); + final List list2 = Arrays.asList("b", "d"); + + // 使用subtract方法 + final Collection subtractResult = CollUtil.subtract(list1, list2); + // 使用subtractToList方法 + final List subtractToListResult = CollUtil.subtractToList(list1, list2); + + // 虽然实现类型不同,但内容应该一致 + assertEquals(new HashSet<>(subtractResult), new HashSet<>(subtractToListResult)); + assertEquals(3, subtractToListResult.size()); + assertTrue(subtractToListResult.contains("a")); + assertTrue(subtractToListResult.contains("c")); + assertTrue(subtractToListResult.contains("e")); + } + @Test public void sortComparableTest() { final List of = ListUtil.toList("a", "c", "b");