image.png

  • 学习所得来源于b站韩顺平java精讲系列

    hashSet底层

  • 底层 是以个 hashMap , 底层进行调用的为hashMap的构造器 ```java public class HashSet extends AbstractSet implements Set, Cloneable, java.io.Serializable { static final long serialVersionUID = -5024744406713321676L;

    private transient HashMap map;

    // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object();

    /**

    • Constructs a new, empty set; the backing HashMap instance has
    • default initial capacity (16) and load factor (0.75). */ public HashSet() { map = new HashMap<>(); }

      /**

    • Constructs a new, empty set; the backing HashMap instance has
    • the specified initial capacity and the specified load factor. *
    • @param initialCapacity the initial capacity of the hash map
    • @param loadFactor the load factor of the hash map
    • @throws IllegalArgumentException if the initial capacity is less
    • than zero, or if the load factor is nonpositive */ public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); } 当然也支持自定义初始大小以及扩容系数,这些本篇后面会提到
  1. <a name="DNWI7"></a>
  2. # hashSet的add
  3. - 前面既然说过hashSet底层为hashMap 那么我们看一下常用方法的调用
  4. ```java
  5. private transient HashMap<E,Object> map;
  6. public boolean add(E e) {
  7. return map.put(e, PRESENT)==null;
  8. }
  9. 可知调用的是map的put方法,那么这个map是从哪里来的?
  10. 前面底层的代码中,有一个初map的定义,实际上还是hashMap,那么其实add方法底层是调用的hashMap的put方法,这点就没有什么疑问了

hashSet的add-hashMap的put

  • 跟随上一个topic的节奏,下面我们进入hashMap的put方法去探究

    进入之前,首先看一下都传入put方法哪些参数
    E e 其实就是我们进行add的元素
    PRESENT 有点意思 其实是上面定义的一个对象常量,当作常量理解即可
    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
    
    public boolean add(E e) {
          return map.put(e, PRESENT)==null;
      }
    ---------------- 来到 hashMap类下的put方法
              K key 是什么? 是add进来的元素,只不过形参名字发生了变化
              V value 是什么? 上面说的那个 PRESENT 常量
    public V put(K key, V value) {
          return putVal(hash(key), key, value, false, true);
     }
    ---------------- 然后接着追之前   有一个hash()方法,来看一眼
              入参 为 add加入进来的元素对象,  如果为空就不进行运算,直接为0,否则拿到key的hashcode  按位异或 无符号位移16位
    static final int hash(Object key) {
          int h;
          return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
      }
    -----------------  ok 接下来我们要看 putVal方法了,这下才是难得地方
      /**
       * The table, initialized on first use, and resized as
       * necessary. When allocated, length is always a power of two.
       * (We also tolerate length zero in some operations to allow
       * bootstrapping mechanics that are currently not needed.)
       */
      transient Node<K,V>[] table; 第一次添加 table 只被声明而未被赋值
    
    /**
       * Implements Map.put and related methods.
       *
       * @param hash add元素经过hash算法取得的hash值
       * @param key  要add的元素本身
       * @param value PRESENT 常量
       * @param onlyIfAbsent if true, don't change existing value
       * @param evict if false, the table is in creation mode.
       * @return previous value, or null if none
       */
      final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                     boolean evict) {
          // 这下面定义的是辅助变量
          Node<K,V>[] tab; 
          Node<K,V> p;
          int n, i;
     // 如果 table == null 或者 tab 的大小为0,则触发第一次扩容,扩容为16个空间的 node节点数组
          if ((tab = table) == null || (n = tab.length) == 0) 
     // 首先他会去判断table是否为空,table是hashMap中声明的一个数组 这个数组的元素为 Node<k,v>节点对象构成
     // 第一次添加元素时 table 只被声明而未被赋值 table 为null 满足条件 ,直接进行扩容
              n = (tab = resize()).length;
          if ((p = tab[i = (n - 1) & hash]) == null)
              tab[i] = newNode(hash, key, value, null);
          else {
              Node<K,V> e; K k;
              if (p.hash == hash &&
                  ((k = p.key) == key || (key != null && key.equals(k))))
                  e = p;
              else if (p instanceof TreeNode)
                  e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
              else {
                  for (int binCount = 0; ; ++binCount) {
                      if ((e = p.next) == null) {
                          p.next = newNode(hash, key, value, null);
                          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                              treeifyBin(tab, hash);
                          break;
                      }
                      if (e.hash == hash &&
                          ((k = e.key) == key || (key != null && key.equals(k))))
                          break;
                      p = e;
                  }
              }
              if (e != null) { // existing mapping for key
                  V oldValue = e.value;
                  if (!onlyIfAbsent || oldValue == null)
                      e.value = value;
                  afterNodeAccess(e);
                  return oldValue;
              }
          }
          ++modCount;
          if (++size > threshold)
              resize();
          afterNodeInsertion(evict);
          return null;
      }
    

    hashMap初始化后的第一次扩容

  • 第一次添加元素的时候的debug截图

  • image.png可以看到table为null,满足第一次添加元素扩容条件
  • 往下走代码
  • image.png
  • 看到第一次添加元素时对table的扩容,扩容出16个空间。

    hashMap扩容添加table中已存在的元素

    image.png

  • 可以看到当前table中存在两个元素 java, addicated,然后我接着向里面添加addicated ,结论肯定是添加不了,但是我们需要关注发生了什么。

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                     boolean evict) {
          Node<K,V>[] tab; Node<K,V> p; int n, i;
          if ((tab = table) == null || (n = tab.length) == 0)
              n = (tab = resize()).length;
    
       // 1 根据key 得到hash 去计算该key应该存放到table表的的哪个索引位置
          // 并把这个位置的对象 赋给p
      // 2 判断p 是否为null
      // 2。1 如果p为null 表示还没有存放过数据,就创建一个 Node(key="addicated",value=PRESENT)
        // 2。2 否则 就放在该位置 tab[i] = newNode(hash, key, value, null);
      //  (n-1) & hash 这行代码先执行, n 为前面拿到的table长度,-1 取余hash得到索引
       // 然后 tab[索引值] 拿到对应的 元素,如果元素不为空,则说明这个索引值下面已经存在元素 进入else分支
          if ((p = tab[i = (n - 1) & hash]) == null)  // 为空 则直接添加进去
              tab[i] = newNode(hash, key, value, null);
          else {
              // 进入else 分支说明 p 当前索引存在元素 p,
              // 局部变量声明,未赋值为null
              Node<K,V> e;
              K k;
               // 如果当前索引位置对应的链表的第一个元素和 准备添加的key(元素)的hash值一样
               // 并且满足 下面两个条件之一
               // 1 。准备加入的key(元素) 和 p 指向的node节点的key 是同一个对象
               // 2 。p指向的node节点 的key的equals()  和准备加入的key比较后相同
               // 就不能加入,
              if (p.hash == hash &&
                  ((k = p.key) == key || (key != null && key.equals(k))))
                  e = p;
               //  再判断 p 是否为一颗红黑树,
              // 如果 p 是一颗红黑树,就调用 putTreeVal 来进行添加
              else if (p instanceof TreeNode)
                  e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
              else {
                   w// 如果table对应索引位置,已经是一个链表,就使用for循环进行比较
              //  1 依次和该链表的每一个元素比较后都不相同  , 则加入到该链表的最后
              //    注意在吧元素添加到链表后,立即判断,该链表是否已经达到 8 个节点
              //     如果达到就调用 treeif'yBin() 对当前这个链表进行树化 转换成红黑树
              //     注意,在转成红黑树时,进行一个判断,如果该table数组的大小 < 64  则先进行扩容
                  //if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                              resize();
             //  只有上面条件不成立的时候,才进行树化,转换成红黑树。
              //  2 依次比较的过程中,如果有相同的情况,就直接break
    
                  for (int binCount = 0; ; ++binCount) {
                      if ((e = p.next) == null) {
                          p.next = newNode(hash, key, value, null);
                          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                              treeifyBin(tab, hash);
                          break;
                      }
                      if (e.hash == hash &&
                          ((k = e.key) == key || (key != null && key.equals(k))))
                          break;
                      p = e;
                  }
              }
              if (e != null) { // existing mapping for key
                  V oldValue = e.value;
                  if (!onlyIfAbsent || oldValue == null)
                      e.value = value;
                  afterNodeAccess(e);
                  return oldValue;
              }
          }
          ++modCount;
          if (++size > threshold)  // size 是没加入一个节点 node size都会 ++ 不管是在 tab中还是在链表上
              resize();
          afterNodeInsertion(evict); // 这是一个空方法,主要是留给hashmap的子类来去实现
          return null;  // 返回一个空则代表添加成功了,否则则是失败
      }
    

    HashSet & linkedHashSet 源码分析&Put方法底层 - 图5

    hashSet 扩容与树化机制

  • hashSet底层是hashMap 第一次添加时,table数组扩容到16,
    临界值(threshold) 是 16*加载因子 (loadFactor) 是0。75 =12

  • 如果table数组使用到了临界值 12 ,就会扩容到16__2 -32,新的临界值就是32 0。75
    依次类推
  • java8中,如果一条链表元素个数到达 TREEIFY_THRESHOLD 默认为8
    平切table的大小 >= MIN_TREEIFY_CAPACIty 默认64
    就会进行树化,转换成红黑树,否则依然采用数组的扩容机制

    hashSet的扩容因子为什么是0.75f?

  • 简单来说是为了防止元素发生碰撞,也就是发生重复导致性能,是一种时间与空间上的取舍。下见链接

  • https://www.cnblogs.com/DarrenChan/p/8854859.html

    linkedHashSet

    1 linkedHashSet 是hashSet的子类
    2 linkedHashset底层是一个linkedHashMap 底层维护了一个数组 + 双向链表
    3 linkedHashSet 根据元素的hashcode 值来决定元素的存储位置,同时使用链表维护元素的次序,使得元素看起来像是以插入顺序保存的
    4 linkedHashSet不允许添加重复元素
    说明
    在linkedHashSet中维护了一个hash表和双向链表 linkedHashSet 有set和tail
    每一个节点有 pre 和 next 属性,这样可以形成双向链表
    在添加一个元素时,先求hash值,再求索引。确定该元素在hashtable的位置,然后添加的元素加入到双向链表(如果已经存在,则不添加,原则与hashset一致
    这样的话 linkedHashSet 也能确保插入顺序和遍历顺序一致

    红黑树相关操作

  • 还未学到,待续