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更精简、语义更准确,是更推荐的做法。
最佳实践建议
- 数据一致性 :如果业务上"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,只需一次遍历:
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);
🔍 进一步优化建议
-
源头去重 :如果
similarProjectVOS的数据来自数据库查询,考虑在 SQL 层面直接去重计数:sqlSELECT COUNT(DISTINCT section_id) FROM ... WHERE ... -
考虑空值处理 :如果
sectionId可能为 null,需要处理:javalong sectionCount = similarProjectVOS.stream() .map(PvModuleVO.SimilarProjectVO::getSectionId) .filter(Objects::nonNull) // 过滤 null 值 .collect(Collectors.toSet()) .size(); -
预分配容量:如果知道大概的去重后数量,可以优化 HashSet 初始容量:
javaint 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, ...] |
格式一样,但括号内内容不同 |
示例演示
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顺序,但去重
实用建议
- 调试时 :不要依赖
Set(特别是HashSet)的toString()顺序来判断数据正确性。 - 需要顺序一致 :
- 使用
List - 或使用
LinkedHashSet(去重但保序) - 或使用
TreeSet(去重且排序)
- 使用
- 在 MyBatis 中 :
- 如果 SQL 条件顺序不影响结果 (如
IN子句),可忽略顺序差异。 - 但如果需要稳定可预测 的 SQL(如调试日志),建议在接口层统一转换为
LinkedHashSet或TreeSet。
- 如果 SQL 条件顺序不影响结果 (如
总结 :idList 和 idSet 打印出的数据通常不一样 (顺序、重复项)。如果你希望它们"看起来一样",需要确保数据无重复 ,并使用 LinkedHashSet 或 TreeSet 来保序。