Set 是一类元素无序(不保证顺序),不可重复、不可索引的集合,Set 接口继承于 Collection 接口。
注:某些 Set 的实现类是有序的,比如 LinkedHashSet。
Collection 有 List 和 Set 两种类型的集合,分别有不同的特点,在开发过程中根据实际应用场景,选择对应类型的集合进行使用。
- List:有序、可重复、可索引的集合,有基于数组数据结构的实现类 ArrayList 和基于双向链表的实现类 LinkedList。如果集合需要存储可以重复的元素,必须选择 List。在此基础上,根据集合的索引查询频率、插入和删除元素频率等场景实际情况,评估选择 ArrayList 或者 LinkedList。
- Set:无序、不可重复、不可索引的集合,有基于哈希表 + 链表 + 红黑树数据结构的实现类 HashSet、基于 HashSet+ 顺序指向链表的实现类 LinkedHashSet、基于红黑树数据结构的实现类 TreeSet。如果集合需要元素去重,则使用 Set。在此基础上,根据是否要求保留数据插入顺序、数据是否有序排列、数据增删查改效率,选择使用 HashSet、LinkedHashSet 或者 TreeSet。
HashSet
HashSet 在 JDK8 之前采用可扩容数组 + 链表的数据结构进行实现,在 JDK8 之后,采用可扩容数组 + 链表 + 红黑树的数据结构进行实现。
HashSet 是基于哈希表的逻辑进行存储数据,因此依赖于 hashCode() 方法。去重判断则依赖于 equals() 方法。因此,HashSet 存储自定义对象,需要在类中重写 hashCode() 和 equals() 方法。这是因为 Object 类中的 equals() 方法默认使用对象内存地址进行判断,也就是说只有同一个对象才返回 true,而一般情况是两个相同类的对象的属性值相同即表示'相等'。而 Object 类中的 hashCode() 是基于内存地址进行计算,同样需要修改为基于属性值进行计算。IDEA 提供一键生成自定义类的重写 hashCode() 和 equals() 方法的操作。
HashSet 对象的元素插入流程如下:
- 调用 hashCode() 方法,计算元素对象的哈希值。
- 用对象的哈希值,和 HashSet 对象的存储元素的数组长度,计算哈希表对应数组的索引。
- 判断数组索引中是否有元素(是否指向 null),如果没有,则存储元素(指向该元素),完成插入操作。
- 若该数组索引中有元素,则遍历链表(可能是链表、也可能是红黑树),调用 equals() 方法与已存储的元素进行逐一比较。若有相同的,则表示元素重复,停止插入操作。若没有重复的,则插入到链表尾部。
LinkedHashSet
LinkedHashSet 是继承于 HashSet 的实现类,与 HashSet 区别在于,LinkedHashSet 是有序的。
相比于 HashSet,LinkedHashSet 中存储元素的 Node 节点,额外增加了两个引用,一个是指向上一个存储元素的 Node 节点,另外一个指向下一个存储元素的 Node 节点,也就是形成了串起插入顺序的双向链表。
用一个例子对比,LinkedHashSet 有序,而 HashSet 无序:
public class App {
public static void main(String[] args) {
// 创建 3 个 Student 对象
Student s1 = new Student("小王", 18);
Student s2 = new Student(, );
(, );
HashSet<Student> hs = <>();
hs.add(s1);
hs.add(s2);
hs.add(s3);
(Student s : hs) {
System.out.println(s);
}
System.out.println();
LinkedHashSet<Student> lhs = <>();
lhs.add(s1);
lhs.add(s2);
lhs.add(s3);
(Student s : lhs) {
System.out.println(s);
}
}
}


