欢迎光临
我们一直在努力

List、Set的相似性

1. 在 MyBatis 的 mapper.xml 文件中,List 和 Set 的遍历方式是完全一样的。

这是因为 MyBatis 的 <foreach> 标签在底层处理时,并不严格区分具体的集合类型(List、Set 等),它统一将它们视为一个 Iterable 对象(或数组)进行处理。

核心机制

<foreach> 标签的核心工作是:遍历一个可迭代的对象。无论是 List 还是 Set,都实现了 java.lang.Iterable 接口,因此 MyBatis 可以用同一套逻辑来遍历它们。

使用示例与对比

假设你的 Mapper 接口方法如下:

// 参数是 List
List<User> selectUsersByIdList(List<Integer> idList);
// 参数是 Set
List<User> selectUsersByIdSet(Set<Integer> idSet);

在 mapper.xml 中,对应的 SQL 写法是一模一样的:

<select id="selectUsersByIdList" resultType="User">
SELECT * FROM user
WHERE id IN
<foreach collection="idList" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>

<select id="selectUsersByIdSet" resultType="User">
SELECT * FROM user
WHERE id IN
<foreach collection="idSet" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>

它们的区别仅在于 Mapper 接口方法参数名不同(idList vs idSet),以及在 XML 中 collection 属性引用的名字不同。<foreach> 标签的内部处理逻辑没有任何区别。

关键注意事项

虽然遍历方式相同,但有几个细节需要注意:

项目说明通用建议
参数名 <foreach> 中的 collection 属性值必须与接口方法中的参数名严格一致。 使用明确的参数名,避免用 list、collection 等默认名。
参数唯一性 当接口方法有且仅有一个集合参数时,在 XML 中可以直接用其参数名引用。 如果方法有多个参数,务必使用 @Param(“key”) 注解显式命名。
有序性 List 是有序的,遍历出的顺序与存入顺序一致;Set(如 HashSet)通常无序。 如果在 SQL 中依赖遍历顺序,请务必使用 List。
去重 List 允许重复元素;Set 会自动去重。 如果希望 IN 语句中的条件自动去重,优先使用 Set。
空集合 两者都可能为空。需在代码或 SQL 中处理空集合,否则可能导致 SQL 语法错误。 在 Java 代码或 MyBatis 的 <if> 标签中判空是良好实践。

总结与建议

  • 遍历方式:完全一样。无需因为 List 或 Set 而改变 XML 中的写法。
  • 选择依据:根据你的业务需求来选择:
    • 需要保持顺序或允许重复,用 List。
    • 需要自动去重且不关心顺序,用 Set。

2. List.size() 和 Set.size()排除重复数据后是一样的

在 Java 中,idList.size() 和 idSet.size() 不一定一样。这取决于原始数据中是否有重复元素。

简单来说:

  • 如果原始数据没有重复,它们的 size() 相等。
  • 如果原始数据有重复,List 会保留所有重复项,而 Set 会自动去重,此时 List 的 size() 会大于 Set 的 size()。

核心区别对比

特性List (如 ArrayList)Set (如 HashSet)对 size() 的影响
是否允许重复元素 允许 不允许(自动去重) 决定 size() 是否相同的关键
顺序 有序(保持插入顺序) 通常无序(HashSet) 与 size() 无关
size() 方法含义 返回列表中元素的总数(包含重复项) 返回集合中唯一元素的数量 计算基数不同

示例说明

import java.util.*;

public class Test {
public static void main(String[] args) {
// 原始数据有重复:1, 2, 2, 3
List<Integer> idList = Arrays.asList(1, 2, 2, 3);
Set<Integer> idSet = new HashSet<>(idList); // 会自动去重

System.out.println("List size: " + idList.size()); // 输出:4
System.out.println("Set size: " + idSet.size()); // 输出:3
System.out.println("size 是否相等: " + (idList.size() == idSet.size())); // 输出:false

// 在MyBatis的SQL中,这会导致:
// List -> WHERE id IN (1, 2, 2, 3) // 条件有4个,但重复条件可能多余
// Set -> WHERE id IN (1, 2, 3) // 条件只有3个,是精确的唯一条件集合
}
}

在你的 MyBatis 场景下的影响

结合你之前关于 MyBatis 的问题,这个区别会产生直接的实际影响:

  • 使用 List:如果传入的 idList 包含重复的用户ID,生成的 SQL 会是 IN (1, 2, 2, 3)。虽然结果正确(数据库会对重复ID返回相同行,结果集不变),但SQL语句更长,可能影响解析效率,且不符合语义。
  • 使用 Set:如果传入的 idSet(或先将 List 转为 Set),生成的 SQL 会是 IN (1, 2, 3)。SQL更精简、语义更准确,是更推荐的做法。

最佳实践建议

  • 数据一致性:如果业务上“ID集合不应重复”,建议在接口层直接使用 Set 类型,或在使用 List 前进行去重,从源头上保证数据语义。
  • 性能考量:对于 contains() 操作(检查某个ID是否存在),HashSet(时间复杂度接近 O(1))的性能远高于 ArrayList(O(n))。如果后续有此类操作,Set 是更好选择。
  • 明确选择:
    • 需要保持顺序或明确允许重复时,用 List。
    • 需要确保唯一性、去重或进行快速查找时,用 Set。
  • 所以,当你不确定传入的ID集合是否有重复时,如果你希望生成的SQL条件是最精简、准确的唯一值,在MyBatis的Mapper接口参数中直接使用 Set 类型是更安全、更语义化的选择。

    3. List去重,由Set可以优化

    1. 避免重复遍历(最优方案)

    使用 Collectors.toSet() 直接收集到 Set,只需一次遍历:

    // 方案1:使用 Collectors.toSet()
    long sectionCount = similarProjectVOS.stream()
    .map(PvModuleVO.SimilarProjectVO::getSectionId)
    .collect(Collectors.toSet())
    .size();

    // 方案2:使用 toCollection 指定 HashSet(可控制初始容量)
    long sectionCount = similarProjectVOS.stream()
    .map(PvModuleVO.SimilarProjectVO::getSectionId)
    .collect(Collectors.toCollection(HashSet::new))
    .size();

    2. 并行流优化(大数据量时)

    当列表非常大(例如超过10万条)时,可考虑并行流:

    long sectionCount = similarProjectVOS.parallelStream()
    .map(PvModuleVO.SimilarProjectVO::getSectionId)
    .collect(Collectors.toSet())
    .size();

    3. 根据实际情况选择数据结构

    如果只需要判断是否有重复,不需要具体数量,用 distinct().count() > 1 即可:

    boolean hasDuplicates = similarProjectVOS.stream()
    .map(PvModuleVO.SimilarProjectVO::getSectionId)
    .distinct()
    .count() > 1;

    📊 性能对比分析

    方案时间复杂度空间复杂度适用场景
    原方案 .distinct().count() O(n) O(k)(k为去重后数量) 小到中等数据集
    优化方案 .collect(Collectors.toSet()).size() O(n) O(k) 推荐:所有场景
    并行流方案 O(n/p)(p为线程数) O(k) 大数据集(>10万条)
    传统 for 循环 O(n) O(k) 极致性能要求

    💡 实际测试对比

    // 性能测试代码示例
    List<PvModuleVO.SimilarProjectVO> data = generateTestData(100000);

    // 原方案
    long start1 = System.nanoTime();
    long count1 = data.stream().map(PvModuleVO.SimilarProjectVO::getSectionId).distinct().count();
    long time1 = System.nanoTime() start1;

    // 优化方案
    long start2 = System.nanoTime();
    long count2 = data.stream().map(PvModuleVO.SimilarProjectVO::getSectionId)
    .collect(Collectors.toSet()).size();
    long time2 = System.nanoTime() start2;

    System.out.printf("原方案: %d 个, 耗时: %.2f ms%n", count1, time1/1e6);
    System.out.printf("优化方案: %d 个, 耗时: %.2f ms%n", count2, time2/1e6);

    🔍 进一步优化建议

  • 源头去重:如果 similarProjectVOS 的数据来自数据库查询,考虑在 SQL 层面直接去重计数:

    SELECT COUNT(DISTINCT section_id) FROM ... WHERE ...

  • 考虑空值处理:如果 sectionId 可能为 null,需要处理:

    long sectionCount = similarProjectVOS.stream()
    .map(PvModuleVO.SimilarProjectVO::getSectionId)
    .filter(Objects::nonNull) // 过滤 null 值
    .collect(Collectors.toSet())
    .size();

  • 预分配容量:如果知道大概的去重后数量,可以优化 HashSet 初始容量:

    int estimatedSize = similarProjectVOS.size() / 2; // 预估容量
    long sectionCount = similarProjectVOS.stream()
    .map(PvModuleVO.SimilarProjectVO::getSectionId)
    .collect(Collectors.toCollection(() -> new HashSet<>(estimatedSize)))
    .size();

  • 🎯 总结推荐

    最佳实践:使用 .collect(Collectors.toSet()).size(),原因:

  • 语义更清晰:明确表达"获取不重复的集合"
  • 性能更优:避免了 distinct() 的中间状态维护
  • 代码简洁:一行完成,易于阅读
  • 如果你的数据量特别大(百万级以上),或者这是性能关键路径,建议先进行基准测试,选择最适合你场景的方案。

    4. 打印效果是一样的

    简单来说,最直观的区别是:List 严格按插入顺序保留所有元素(包括重复项);而典型的 Set(如 HashSet)不保证顺序且自动去重。它们的 toString() 输出格式通常也不同(List 是 [a, b, c],Set 是 [a, c, b] 等)。

    核心区别对比

    特性List (如 ArrayList)Set (如 HashSet)对打印输出的影响
    顺序性 严格保持插入顺序 通常不保证顺序(HashSet基于哈希值) 最常见区别:打印顺序可能不同
    重复元素 保留所有重复项 自动去重(只保留一个) 有重复数据时,List打印更长
    toString() 格式 格式相同,都为 [元素1, 元素2, …] 格式相同,都为 [元素1, 元素2, …] 格式一样,但括号内内容不同

    示例演示

    import java.util.*;

    public class PrintDemo {
    public static void main(String[] args) {
    // 原始数据:有重复,注意插入顺序
    List<Integer> idList = new ArrayList<>(Arrays.asList(3, 1, 2, 1, 4));
    Set<Integer> idSet = new HashSet<>(idList); // 用List构造Set会自动去重

    System.out.println("idList.toString() = " + idList);
    // 输出:idList.toString() = [3, 1, 2, 1, 4] ← 保持顺序和重复
    System.out.println("idSet.toString() = " + idSet);
    // 可能输出:idSet.toString() = [1, 2, 3, 4] ← 顺序不定,已去重
    // 也可能是:[3, 1, 2, 4] 等,取决于HashSet内部实现

    // 用迭代器/for循环打印,差异更明显
    System.out.print("遍历idList: ");
    for (Integer id : idList) { System.out.print(id + " "); }
    // 输出:遍历idList: 3 1 2 1 4

    System.out.print("\\n遍历idSet : ");
    for (Integer id : idSet) { System.out.print(id + " "); }
    // 可能输出:遍历idSet : 1 2 3 4
    }
    }

    三种关键场景分析

    场景List 打印Set 打印输出是否“相同”
    数据完全无重复 [A, B, C] [A, B, C] 或 [B, C, A] 等 元素集合相同,但顺序可能不同
    数据有重复 [A, B, B, C] [A, B, C] 等 元素集合不同(数量、内容)
    使用有序Set(如 TreeSet, LinkedHashSet) [A, B, C] TreeSet: 排序后 [A, B, C]LinkedHashSet: 保持插入顺序 [A, B, C] 可能相同(见下文)

    特殊说明:如果你使用 LinkedHashSet,它会保持插入顺序;如果使用 TreeSet,它会按自然排序。对于无重复且顺序匹配的数据,它们的打印可能和 List“看起来一样”。

    在 MyBatis 场景下的实际影响

    回到你最初关心的 MyBatis 使用场景,这个差异会直接反映在生成的 SQL 中:

    — 假设原始数据:List<Integer> ids = Arrays.asList(3, 1, 2, 1, 4)
    — 使用 List 参数
    WHERE id IN (3, 1, 2, 1, 4) — 顺序固定,有重复
    — 使用 HashSet 参数
    WHERE id IN (1, 2, 3, 4) — 顺序可能改变,已去重
    — 使用 LinkedHashSet 参数(用 new LinkedHashSet<>(list) 构造)
    WHERE id IN (3, 1, 2, 4) — 保持原List顺序,但去重

    实用建议

  • 调试时:不要依赖 Set(特别是 HashSet)的 toString() 顺序来判断数据正确性。
  • 需要顺序一致:
    • 使用 List
    • 或使用 LinkedHashSet(去重但保序)
    • 或使用 TreeSet(去重且排序)
  • 在 MyBatis 中:
    • 如果 SQL 条件顺序不影响结果(如 IN 子句),可忽略顺序差异。
    • 但如果需要稳定可预测的 SQL(如调试日志),建议在接口层统一转换为 LinkedHashSet 或 TreeSet。
  • 总结:idList 和 idSet 打印出的数据通常不一样(顺序、重复项)。如果你希望它们“看起来一样”,需要确保数据无重复,并使用 LinkedHashSet 或 TreeSet 来保序。

    赞(0)
    未经允许不得转载:171主机测评 » List、Set的相似性
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址