关于 HashSet

  • HashSet 实现了 Set 接口;

    1. public class HashSet<E>
    2. extends AbstractSet<E>
    3. implements Set<E>, Cloneable, java.io.Serializable
    4. {
    5. static final long serialVersionUID = -5024744406713321676L;
    6. private transient HashMap<E,Object> map;
    7. // Dummy value to associate with an Object in the backing Map
    8. private static final Object PRESENT = new Object();
  • 不能有重复元素 / 对象,在前面 Set 接口使用时已经提到过。

  • 可以存放 null 值,但因为元素不可重复,只能放一个 null;
  • HashSet 底层实际上是 HashMap,而 HashMap 的底层实际上是数组+链表+红黑树(JDK 7 是数组+链表、JDK 8 是数组+链表+红黑树、JDK X 是… ,此处以 JDK 8 为例说明);
  • HashSet 不保证元素是有序的,取决于 hash 后,再确定索引的结果;

构造方法一览

image.png

  1. /**
  2. * Constructs a new, empty set; the backing HashMap instance has
  3. * default initial capacity (16) and load factor (0.75).
  4. */
  5. public HashSet() {
  6. map = new HashMap<>();
  7. }
  8. /**
  9. * Constructs a new set containing the elements in the specified
  10. * collection. The <tt>HashMap</tt> is created with default load factor
  11. * (0.75) and an initial capacity sufficient to contain the elements in
  12. * the specified collection.
  13. *
  14. * @param c the collection whose elements are to be placed into this set
  15. * @throws NullPointerException if the specified collection is null
  16. */
  17. public HashSet(Collection<? extends E> c) {
  18. map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
  19. addAll(c);
  20. }
  21. /**
  22. * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
  23. * the specified initial capacity and the specified load factor.
  24. *
  25. * @param initialCapacity the initial capacity of the hash map
  26. * @param loadFactor the load factor of the hash map
  27. * @throws IllegalArgumentException if the initial capacity is less
  28. * than zero, or if the load factor is nonpositive
  29. */
  30. public HashSet(int initialCapacity, float loadFactor) {
  31. map = new HashMap<>(initialCapacity, loadFactor);
  32. }
  33. /**
  34. * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
  35. * the specified initial capacity and default load factor (0.75).
  36. *
  37. * @param initialCapacity the initial capacity of the hash table
  38. * @throws IllegalArgumentException if the initial capacity is less
  39. * than zero
  40. */
  41. public HashSet(int initialCapacity) {
  42. map = new HashMap<>(initialCapacity);
  43. }

扩容机制

添加一个元素时,先得计算这个元素的 hash 值,查存储数据表 table 中,这个索引位置是否已经存放了元素。如果没有,直接放入数据表 table 中,如果有,调用元素的 equals 方法比较,如果相同,就放弃添加,如果不相同,则添加到链表的末尾。在 Java 8 中,如果一条链表的元素个数大等于 TREEIFY_THRESHOLD( 默认 8 ) - 1,并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认 64 ),就会将该索引位置的链表转换为红黑树。

例题

1、唯一性

如何理解 HashSet 不能有重复元素 / 对象?看一下下面的例题,思考一下。

  1. public class Main {
  2. public static void main(String[] args) {
  3. Set set = new HashSet();
  4. // 添加重复元素测试
  5. System.out.println("添加重复元素测试");
  6. System.out.println(set.add("test string"));// true
  7. System.out.println(set.add("test string"));// false
  8. System.out.println();
  9. // 添加值相同的对象测试
  10. System.out.println("添加值相同的对象测试");
  11. System.out.println(set.add(new Dog("jerry", 3)));// true
  12. System.out.println(set.add(new Dog("tom", 4)));// true
  13. System.out.println(set.add(new Dog("tom", 4)));// true 思考一下,为什么?
  14. System.out.println();
  15. // 再加深一下,添加 String 对象测试
  16. System.out.println("添加 String 对象测试");
  17. System.out.println(set.add(new String("123")));// true
  18. System.out.println(set.add(new String("123")));// false
  19. System.out.println(set.add("123"));// false
  20. System.out.println();
  21. }
  22. }
  23. class Dog {
  24. String name;
  25. int age;
  26. public Dog(String name, int age) {
  27. this.name = name;
  28. this.age = age;
  29. }
  30. @Override
  31. public String toString() {
  32. return "Dog{" +
  33. "name='" + name + '\'' +
  34. ", age=" + age +
  35. '}';
  36. }
  37. }

2、无序性

HashSet 是不保证元素有序的,元素的顺序取决于 hash 后索引的结果。即 HashSet 无法保证在操作元素后,既有元素的顺序保持不变。

public class Main {
    public static void main(String[] args) {
        Set set = new HashSet();

        set.add(new Integer(5));
        set.add(6);
        set.add("qwe");
        System.out.println(set);// [5, 6, qwe]

        set.add("16546");
        System.out.println(set);// [5, 6, 16546, qwe]
    }
}

常用方法源码分析

1、HashSet add

https://www.yuque.com/jaded/fz8wr7/eb8nft

2、HashMap.rezise

当 HashSet 调用 add 方法添加元素时,实际上底层调用的是 HashMap 的 put 方法,所以,讲 HashSet 的扩容实际上就是在讲 HashMap 的扩容机制。
https://www.yuque.com/jaded/fz8wr7/bwl5vw

3、treeifyBin

https://www.yuque.com/jaded/fz8wr7/gr4tnv

问题

1、为什么说 HashSet 的底层是 HashMap?

先看一下 HashSet 的 map 属性:

private transient HashMap<E,Object> map;

再看一下 HashSet 的无参构造器:

public HashSet() {
    map = new HashMap<>();
}

可以看到 HashSet 对象被实例化时,底层创建了一个 HashMap 对象赋给了自身的 map 属性。正因为 HashSet 的各个操作(例如 add / remove / clear 等等)都是基于自身这个 map 属性扩展的,所以我们说 HashSet 的底层是 HashMap 。

PS:需要知道的是,JDK 7 的 HashMap 是基于数组+链表实现的、JDK 8 的 HashMap 是基于数组+链表/红黑树实现的、