Set集合类_演练
引入
HashMap的值是唯一的吗?
import java.util.HashMap;
import java.util.Map;
public class MyTest {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("zhangsan", "123-456-7890");
map.put("lisi", "234-567-8901");
map.put("wangwu", "345-678-9012");
// Map的特点是 【键】必须是唯一的
// HashMap的特点是 【键】必须是唯一的 + 无序
// 值是唯一的吗?
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
System.out.println("姓名: " + key + ", 电话: " + value);
}
}
}
尝试将值改为同一个,例如字符串123
,甚至是null
都可以正常运行。因此,值并不是唯一的。
但HashMap
的键,同时满足唯一和无序。
实际上把HashMap
中的键单独拎出来就是HashSet
:
- Set的特点是【元素】必须唯一
- HashSet的特点是:唯一 + 无序
HashSet
可译成散列集。
HashSet
简介
摘自官方文档:
This class implements the Set interface, backed by a hash table (actually a HashMap instance). It makes no guarantees as to the iteration order of the set; in particular, it does not guarantee that the order will remain constant over time.
该类实现了 Set
接口,底层使用哈希表(实际上是一个 HashMap
实例)。它不保证集合的迭代顺序,特别是,它并不保证顺序会随时间保持不变。
也就是说,HashSet
就是基于HashMap
实现的,也就是我们刚才说的,单独把“键”拎出来。(这就是为何先学HashMap
然后再学HashSet
)
如果查看源码的话,会发现HashSet
实际上维护了一个HashMap
。
主要方法
见书P256。(拓展:你能参考方法表自己实现一个HashSet吗?)
应用例子
那这样一个只有键的哈希表,换句话说只能存名字、不能存电话的电话本,有什么用处呢?
先看下如何将一个HashMap
“改”成HashSet
。实际上就是把键给拎出来,把值去掉,换个类名、方法名:
import java.util.HashSet;
import java.util.Set;
public class MyTest {
public static void main(String[] args) {
// Map<String, String> map = new HashMap<>();
Set<String> set = new HashSet<>(); // 同理,set是一个接口,必须依靠于实现类HashSet才能用
set.add("zhangsan");
set.add("lisi");
set.add("wangwu");
for (String s : set) {
System.out.println(s);
}
}
}
最典型的应用是检测重复的用户输入,假设你在开发一个系统,用户需要填写用户名。为确保用户名是唯一的,就可以使用HashSet
来检测是否存在重复输入:
import java.util.HashSet;
import java.util.Scanner;
public class UniqueUsernameChecker {
public static void main(String[] args) {
HashSet<String> usernames = new HashSet<>(); // 创建一个散列集用于保存用户名
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("用户名: ");
String username = scanner.nextLine();
if (username.equalsIgnoreCase("exit")) {
// 如果输入exit就退出循环,这个exit不区分大小写
break;
}
// 可以用contains检查是否重复
if (usernames.contains(username)) {
System.out.println("用户名已存在,请输入其他用户名!");
} else {
usernames.add(username);
System.out.println("用户名 " + username + " 已成功添加!");
}
}
System.out.println("\n所有用户名: ");
System.out.println(usernames); // HashSet 覆盖了 toString 方法
}
}
实际中,这个工作通常是数据库层面去做的,但是基本原理一致。
HashSet
基于HashMap
实现
通过查看源码,发现HashSet
实际上维护了一个HashMap
:
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
@java.io.Serial
static final long serialVersionUID = -5024744406713321676L;
transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
static final Object PRESENT = new Object();
当创建一个HashSet
时,也会初始化一个HashMap
,见HashSet
的构造器(当new
一个HashSet
时下面代码会被执行):
public HashSet() {
map = new HashMap<>();
}
当调用add
方法时,实际上是调用了一个map.put
方法:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
包括上面代码所调用的contains
,实际上是调用了containsKey
:
public boolean contains(Object o) {
return map.containsKey(o);
}
小结
HashSet
的特点:唯一性 + 无序性- 主要方法:
add
方法、contains
方法、remove
方法(后续练习体现) HashSet
的底层实际上是一个HashMap
实例
练习
练习1
编写一个程序,完成以下操作:
- 创建一个
HashSet
来存储整数。 - 向集合中添加以下数字:10, 20, 30, 20, 40, 10
- 输出集合的内容。
- 判断集合是否包含数字 30。如果包含输出“包含30”,否则输出“不包含30”。
- 删除数字 20
- 再次输出集合内容。
参考代码:
import java.util.HashSet;
import java.util.Set;
public class MyTest {
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
// 尖括号中的类型参数不能是基本类型,也就是不能为int
// 需要使用int的升级版本 或者说类版本, 也就是所谓的包装器
set.add(10); // 当将一个int值赋给 Integer类型变量时会进行自动装箱
// 上面相当于 set.add(Integer.valueOf(10));
// 这个工作是编译器做的
set.add(20);
set.add(30);
set.add(20);
set.add(40);
set.add(10);
System.out.println(set);
if (set.contains(30)) {
System.out.println("包含30");
} else {
System.out.println("不包含30");
}
set.remove(20);
System.out.println(set);
}
}
结合代码进一步:
- 理解 HashSet 的无序性(输出顺序可能与插入顺序不同)。
- 理解 Set 中元素的唯一性(相同元素不会重复存储)。
练习2
需要澄清的是无序性是HashSet
的特性,并不是Set
的特性。Set
的特性只有元素的唯一性。
有一种Set
可以保存元素的顺序。它是HashSet
的子类,叫LinkedHashSet
。
可通过如下方式创建一个LinkedHashSet
,其用法和HashSet
一致,只是它可以保存元素的顺序:
Set<String> set = new LinkedHashSet<>();
可用如下方式将一个list
转换为LinkedHashSet
:
Set<String> set = new LinkedHashSet<>(list);
此时,list
中的元素顺序将维持不变,但重复添加的元素将被去除。如果想把set
中元素重新倒回list
中去,可以采用如下的代码:
list.clear(); // 清空原有的元素
list.addAll(set); // 把 LinkedHashSet 中的元素重新 按顺序 添加进list中
基于上述信息,请按以下步骤完成练习:
- 创建一个
list
,实现类选LinkedList
- 在
list
中依次添加字符串元素:zhangsan, lisi, lisi, wangwu, lisi, wangwu
- 直接通过
System.out.println
输出list
中的元素 - 通过
LinkedHashSet
清除list
中的重复元素 - 再次通过
System.out.println
输出list
中的元素
参考输出:
[zhangsan, lisi, lisi, wangwu, lisi, wangwu]
[zhangsan, lisi, wangwu]
练习3(★★★★★)
已知数组存放一批QQ号码,QQ号码最长为11位,最短为5位String[] strs = {"12345","67891","12347809933","98765432102","67891","12347809933"}
。 将该数组里面的所有qq号都存放在LinkedList
中,将list
中重复的元素删除,将list
中所有元素分别用迭代器
和增强for循环
打印出来。
PS. 迭代器版本暂时不做。
可供直接复制的代码:
String[] strings = {"12345","67891","12347809933","98765432102","67891","12347809933"};