List、Set的相似性

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

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

核心机制

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

使用示例与对比

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

java 复制代码
// 参数是 List
List<User> selectUsersByIdList(List<Integer> idList);
// 参数是 Set
List<User> selectUsersByIdSet(Set<Integer> idSet);

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

xml 复制代码
<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() 方法含义 返回列表中元素的总数(包含重复项) 返回集合中唯一元素的数量 计算基数不同

示例说明

java 复制代码
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更精简、语义更准确,是更推荐的做法。

最佳实践建议

  1. 数据一致性 :如果业务上"ID集合不应重复",建议在接口层直接使用 Set 类型,或在使用 List 前进行去重,从源头上保证数据语义。
  2. 性能考量 :对于 contains() 操作(检查某个ID是否存在),HashSet(时间复杂度接近 O(1))的性能远高于 ArrayList(O(n))。如果后续有此类操作,Set 是更好选择。
  3. 明确选择 :
    • 需要保持顺序 或明确允许重复 时,用 List。
    • 需要确保唯一性 、去重 或进行快速查找 时,用 Set。

所以,当你不确定传入的ID集合是否有重复时,如果你希望生成的SQL条件是最精简、准确的唯一值,在MyBatis的Mapper接口参数中直接使用 Set 类型是更安全、更语义化的选择。

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

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

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

java 复制代码
// 方案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万条)时,可考虑并行流:

java 复制代码
long sectionCount = similarProjectVOS.parallelStream()
        .map(PvModuleVO.SimilarProjectVO::getSectionId)
        .collect(Collectors.toSet())
        .size();

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

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

java 复制代码
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) 极致性能要求

💡 实际测试对比

java 复制代码
// 性能测试代码示例
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);

🔍 进一步优化建议

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

    sql 复制代码
    SELECT COUNT(DISTINCT section_id) FROM ... WHERE ...
  2. 考虑空值处理 :如果 sectionId 可能为 null,需要处理:

    java 复制代码
    long sectionCount = similarProjectVOS.stream()
            .map(PvModuleVO.SimilarProjectVO::getSectionId)
            .filter(Objects::nonNull)  // 过滤 null 值
            .collect(Collectors.toSet())
            .size();
  3. 预分配容量:如果知道大概的去重后数量,可以优化 HashSet 初始容量:

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

🎯 总结推荐

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

  1. 语义更清晰:明确表达"获取不重复的集合"
  2. 性能更优 :避免了 distinct() 的中间状态维护
  3. 代码简洁:一行完成,易于阅读

如果你的数据量特别大(百万级以上),或者这是性能关键路径,建议先进行基准测试,选择最适合你场景的方案。

4. 打印效果是一样的

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

核心区别对比

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

示例演示

java 复制代码
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 中:

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顺序,但去重

实用建议

  1. 调试时 :不要依赖 Set(特别是 HashSet)的 toString() 顺序来判断数据正确性。
  2. 需要顺序一致 :
    • 使用 List
    • 或使用 LinkedHashSet(去重但保序)
    • 或使用 TreeSet(去重且排序)
  3. 在 MyBatis 中 :
    • 如果 SQL 条件顺序不影响结果 (如 IN 子句),可忽略顺序差异。
    • 但如果需要稳定可预测 的 SQL(如调试日志),建议在接口层统一转换为 LinkedHashSet 或 TreeSet。

总结 :idList 和 idSet 打印出的数据通常不一样 (顺序、重复项)。如果你希望它们"看起来一样",需要确保数据无重复 ,并使用 LinkedHashSet 或 TreeSet 来保序。

相关推荐
律宏阔10 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔10 小时前
Windows 禁用联想笔记本所有 Lenovo 后台服务
windows
律宏阔10 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
小羊没烦恼!6 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
rockmelodies6 天前
# Windows Server2008 R2 Standard(SP1镜像U盘)重置本地管理员密码【完整详细步骤】
windows·密码破解
需要8266 天前
JVM 内存区域与对象创建:一次 GC 从哪来
java·jvm·spring boot·spring·servlet·tomcat
kakakahahahaha6 天前
Windows C盘空间不足的安全清理与迁移流程
windows·电脑·笔记本电脑·软件需求·c盘清理
做咩啊~6 天前
远程连接提示:身份验证错误,要求的函数不受支持
windows
2601_953824616 天前
基于SSM的水果超市管理系统
java·tomcat·maven
百事牛科技6 天前
PPT只读怎么取消?四种情况分别处理
windows·powerpoint