Set集合竟然是有序集合!?错!
问题发现的背景
最近刚刚开启为应对暑期实习的第一轮Java复习,每遇到一个知识点我都习惯地手敲代码实现或者求证一下,而不是硬背内容。
而在复习到 List、Set、Map 之间的去别的时候,我对Set集合进行了初始化,往初始化后的对象添加了三个值 a、b、c
,并分别利用迭代器和Stream方法对其遍历取值,发现无论我执行多少次,其输出结果都按照添加顺序进行输出:
通过迭代器获取:
a
b
c
通过Stream方法获取:
a
b
c
甚至我进行了多次遍历并记录结果,所输出结果依旧相同。
代码:
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class HashSetOrderTest {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c");
// 多次遍历并记录结果
for (int i = 0; i < 5; i++) {
System.out.println("遍历结果 " + (i + 1) + ":");
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}
System.out.println();
}
}
}
结果:
遍历结果 1:
a
b
c
遍历结果 2:
a
b
c
遍历结果 3:
a
b
c
遍历结果 4:
a
b
c
遍历结果 5:
a
b
c
Set 是否是无序集合?
我们通过Java入门课就得知了 Set集合 是一个无序、不可重复地集合,那为什么在这里,它带给我们的感觉却是和 List 一样有序呢?
首先,答案是否定的;
当我们添加更多个元素,在进行求证的时候,就会发现,它每一次输出的结果是不相同的。但如果只是因为这个,并不值得我花十五分钟写一篇博客。。
我们得从中找出原因。
我们尝试阅读源码,可以发现,HashSet类的底层实现是直接使用了HashMap类。
那么我们就可以从HashMap的实现中找出其原因,我们可以先来明确下哈希表的基本定义:
哈希表是一种键值对的数据结构,它使用哈希函数将键映射到值。哈希函数将键转换为一个整数,该整数用于确定值在哈希表中的位置。
在可以确定哈希表的基本定义后,这个问题就转变成一个哈希问题了。而到这里,我就不得不联想到了哈希冲突。
- 首先,我们知道造成哈希冲突的原因是不同的元素存储在哈希表的同一个位置而导致的。
- 其次,在Java中,HashSet类使用HashMap类作为其底层实现。HashMap类使用拉链法来解决哈希冲突。拉链法是指将具有相同哈希值的键存储在链表中。
- 接着,如果两个或多个元素具有相同的哈希值,则它们将存储在哈希表的同一个位置。那么进行读取的时候,它就会在同一个位置上任意取出一个值作为下一个输出的值,这就导致了在较多元素的Set集合中,每次读出的结果不同。
- 最后,我们可以得知,当在越小的Set集合中,哈希冲突的可能性越小,所以当我们只有三个元素的时候,且他们并没有发生哈希冲突的时候,自然每次读取的顺序不会改变。
因此,在JDK的实现中,对于较小(拥有较少元素)的HashSet,每次读出的结果都相同,这是因为Set集合的底层实现使用了哈希表。Set集合中的元素通常会存储在哈希表的不同位置。这意味着,iterator()方法和stream()方法遍历集合时,会以相同的顺序访问元素。
以下示例说明了哈希表如何存储 Set集合中的元素:
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c");
// 使用HashMap类查看Set集合的底层实现
HashMap<String, Object> map = (HashMap<String, Object>) set;
// 遍历HashMap中的键值对
for (Map.Entry<String, Object> entry : map.entrySet()) {
System.out.println(entry.getKey());
}
List、Set、Map的区别
最后,怕你们觉得这个问题很无聊,特此供上三个集合的区别汇总表。(但我个人是认为这样的复习方式挺有意思的,既让我对集合之间的区别有了更深的理解,也让我同时去复习到了哈希表、哈希冲突、哈希算法)
特性 | List | Set | Map |
---|---|---|---|
接口 | Collection | Collection | 无 |
元素存储 | 有序、可重复 | 无序、不可重复 | 无序、键不可重复 |
遍历方式 | for循环、迭代器 | 迭代器 | 迭代器、entrySet() |
常用方法 | add() 、get() 、remove() 、size() 、contains 、indexOf() 、lastIndexOf() | add() 、remove() 、contains() 、isEmpty() 、size() | put() 、get() 、remove() 、containsKey() 、containsValue() 、isEmpty() 、size() 、entrySet() |
使用场景 | 存储有序元素 | 存储无序、不重复元素 | 存储键值对 |