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()。
核心区别对比
| 是否允许重复元素 | 允许 | 不允许(自动去重) | 决定 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更精简、语义更准确,是更推荐的做法。
最佳实践建议
- 需要保持顺序或明确允许重复时,用 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(),原因:
如果你的数据量特别大(百万级以上),或者这是性能关键路径,建议先进行基准测试,选择最适合你场景的方案。
4. 打印效果是一样的
简单来说,最直观的区别是:List 严格按插入顺序保留所有元素(包括重复项);而典型的 Set(如 HashSet)不保证顺序且自动去重。它们的 toString() 输出格式通常也不同(List 是 [a, b, c],Set 是 [a, c, b] 等)。
核心区别对比
| 顺序性 | 严格保持插入顺序 | 通常不保证顺序(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
}
}
三种关键场景分析
| 数据完全无重复 | [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顺序,但去重
实用建议
- 使用 List
- 或使用 LinkedHashSet(去重但保序)
- 或使用 TreeSet(去重且排序)
- 如果 SQL 条件顺序不影响结果(如 IN 子句),可忽略顺序差异。
- 但如果需要稳定可预测的 SQL(如调试日志),建议在接口层统一转换为 LinkedHashSet 或 TreeSet。
总结:idList 和 idSet 打印出的数据通常不一样(顺序、重复项)。如果你希望它们“看起来一样”,需要确保数据无重复,并使用 LinkedHashSet 或 TreeSet 来保序。






