我们知道,Map用于存储key-value的映射,对于充当key的对象,是不能重复的,并且,不但需要正确覆写equals()方法,还要正确覆写hashCode()方法。

如果我们只需要存储不重复的key,并不需要存储映射的value,那么就可以使用Set

Set用于存储不重复的元素集合,它主要提供以下几个方法:

  • 将元素添加进Set<E>boolean add(E e)
  • 将元素从Set<E>删除:boolean remove(Object e)
  • 判断是否包含元素:boolean contains(Object e)

我们来看几个简单的例子:

  1. import java.util.*;
  2. public class Main {
  3. public static void main(String[] args) {
  4. Set<String> set = new HashSet<>();
  5. System.out.println(set.add("abc")); // true
  6. System.out.println(set.add("xyz")); // true
  7. System.out.println(set.add("xyz")); // false,添加失败,因为元素已存在
  8. System.out.println(set.contains("xyz")); // true,元素存在
  9. System.out.println(set.contains("XYZ")); // false,元素不存在
  10. System.out.println(set.remove("hello")); // false,删除失败,因为元素不存在
  11. System.out.println(set.size()); // 2,一共两个元素
  12. }
  13. }

Set实际上相当于只存储key、不存储value的Map。我们经常用Set用于去除重复元素。

因为放入Set的元素和Map的key类似,都要正确实现equals()hashCode()方法,否则该元素无法正确地放入Set

最常用的Set实现类是HashSet,实际上,HashSet仅仅是对HashMap的一个简单封装,它的核心代码如下:

  1. public class HashSet<E> implements Set<E> {
  2. // 持有一个HashMap:
  3. private HashMap<E, Object> map = new HashMap<>();
  4. // 放入HashMap的value:
  5. private static final Object PRESENT = new Object();
  6. public boolean add(E e) {
  7. return map.put(e, PRESENT) == null;
  8. }
  9. public boolean contains(Object o) {
  10. return map.containsKey(o);
  11. }
  12. public boolean remove(Object o) {
  13. return map.remove(o) == PRESENT;
  14. }
  15. }

Set接口并不保证有序,而SortedSet接口则保证元素是有序的:

  • HashSet是无序的,因为它实现了Set接口,并没有实现SortedSet接口;
  • TreeSet是有序的,因为它实现了SortedSet接口。

用一张图表示:

  1. ┌───┐
  2. Set
  3. └───┘
  4. ┌────┴─────┐
  5. ┌───────┐ ┌─────────┐
  6. HashSet SortedSet
  7. └───────┘ └─────────┘
  8. ┌─────────┐
  9. TreeSet
  10. └─────────┘

我们来看HashSet的输出:

  1. import java.util.*;
  2. public class Main {
  3. public static void main(String[] args) {
  4. Set<String> set = new HashSet<>();
  5. set.add("apple");
  6. set.add("banana");
  7. set.add("pear");
  8. set.add("orange");
  9. for (String s : set) {
  10. System.out.println(s);
  11. }
  12. }
  13. }

注意输出的顺序既不是添加的顺序,也不是String排序的顺序,在不同版本的JDK中,这个顺序也可能是不同的。

HashSet换成TreeSet,在遍历TreeSet时,输出就是有序的,这个顺序是元素的排序顺序:

  1. import java.util.*;
  2. public class Main {
  3. public static void main(String[] args) {
  4. Set<String> set = new TreeSet<>();
  5. set.add("apple");
  6. set.add("banana");
  7. set.add("pear");
  8. set.add("orange");
  9. for (String s : set) {
  10. System.out.println(s);
  11. }
  12. }
  13. }

使用TreeSet和使用TreeMap的要求一样,添加的元素必须正确实现Comparable接口,如果没有实现Comparable接口,那么创建TreeSet时必须传入一个Comparator对象。

练习

在聊天软件中,发送方发送消息时,遇到网络超时后就会自动重发,因此,接收方可能会收到重复的消息,在显示给用户看的时候,需要首先去重。请练习使用Set去除重复的消息:

  1. import java.util.*;
  2. public class Main {
  3. public static void main(String[] args) {
  4. List<Message> received = List.of(
  5. new Message(1, "Hello!"),
  6. new Message(2, "发工资了吗?"),
  7. new Message(2, "发工资了吗?"),
  8. new Message(3, "去哪吃饭?"),
  9. new Message(3, "去哪吃饭?"),
  10. new Message(4, "Bye")
  11. );
  12. List<Message> displayMessages = process(received);
  13. for (Message message : displayMessages) {
  14. System.out.println(message.text);
  15. }
  16. }
  17. static List<Message> process(List<Message> received) {
  18. // TODO: 按sequence去除重复消息
  19. return received.stream().distinct().collect(Collectors.toList());
  20. }
  21. }
  22. class Message {
  23. public final int sequence;
  24. public final String text;
  25. public Message(int sequence, String text) {
  26. this.sequence = sequence;
  27. this.text = text;
  28. }
  29. @Override
  30. public boolean equals(Object o) {
  31. if (this == o) return true;
  32. if (o == null || getClass() != o.getClass()) return false;
  33. Message message = (Message) o;
  34. return sequence == message.sequence;
  35. }
  36. @Override
  37. public int hashCode() {
  38. return Objects.hash(sequence);
  39. }
  40. }

小结

Set用于存储不重复的元素集合:

  • 放入HashSet的元素与作为HashMap的key要求相同;
  • 放入TreeSet的元素与作为TreeMap的Key要求相同;

利用Set可以去除重复元素;

遍历SortedSet按照元素的排序顺序遍历,也可以自定义排序算法。