List、Set的相似性

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

这是因为 MyBatis 的 <foreach> 标签在底层处理时,并不严格区分具体的集合类型(ListSet 等),它统一将它们视为一个 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 属性值必须与接口方法中的参数名严格一致。 使用明确的参数名,避免用 listcollection 等默认名。
参数唯一性 当接口方法有且仅有一个集合参数时,在 XML 中可以直接用其参数名引用。 如果方法有多个参数,务必使用 @Param("key") 注解显式命名。
有序性 List 是有序的,遍历出的顺序与存入顺序一致;Set(如 HashSet)通常无序。 如果在 SQL 中依赖遍历顺序,请务必使用 List
去重 List 允许重复元素;Set 会自动去重。 如果希望 IN 语句中的条件自动去重,优先使用 Set
空集合 两者都可能为空。需在代码或 SQL 中处理空集合,否则可能导致 SQL 语法错误。 在 Java 代码或 MyBatis 的 <if> 标签中判空是良好实践。

总结与建议

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

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

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

简单来说:

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

核心区别对比

特性 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(如调试日志),建议在接口层统一转换为 LinkedHashSetTreeSet

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

相关推荐
修炼室2 小时前
在 Windows 上构建不占 C 盘的科研级 WSL 实验环境(Ubuntu 22.04)
c语言·windows·ubuntu
玖釉-2 小时前
[Vulkan 学习之路] 04 - 选妃环节:挑选物理设备与队列族
c++·windows·图形渲染
半壶清水2 小时前
如何在IDEA中将JavaFX项目打包EXE文件
java·windows·intellij-idea·jar
_oP_i3 小时前
Windows 下往 Elasticsearch 添加数据
大数据·windows·elasticsearch
玖釉-3 小时前
[Vulkan 学习之路] 10 - 掌握 SPIR-V:编写你的第一个着色器 (Shader Modules)
c++·windows·图形渲染
玖釉-3 小时前
[Vulkan 学习之路] 03 - 你的守护天使:校验层 (Validation Layers)
c++·windows·图形渲染
xiaoliuliu123453 小时前
apache-tomcat-6.0.10使用步骤详解(附启动与部署教程)
java·tomcat·apache
NimoXie3 小时前
Windows CUDA + cuDNN + TensorFlow + PyTorch 识别 GPU 的简单整合
pytorch·windows·tensorflow
麦兜*3 小时前
Spring Boot整合MyBatis-Plus实战:简化CRUD操作的最佳实践
spring boot·tomcat·mybatis