3.2.6 Set接口及其实现类

Set接口及其 实现类


基本
  1. 无序(添加和取出的顺序不一样),没有索引
  2. 不允许有重复的元素
  3. 常用方法和Collection的子接口相同
    • 遍历的方式和Collection 相同
    • 可以使用iterator和增强for 但是不能使用索引的方式来获取
  4. 补充第一点:取出的顺序不是添加的顺序,但是取出后 顺序固定

Set接口实现类-HashSet
(1)HashSet基础说明:
  1. HashSet实现了Set接口

  2. HashSet实际上是基于HashMap实现的

    public HashSet(){
    	map = new HashMap<>();
    }
    
  3. 可以存放null值,但是只能有一个null

  4. HashSet不保证元素是有序的,取决于hash后,在确定索引的结果

  5. 不能有重复的元素 / 对象


(2)HashSet底层机制说明:
  • HashSet底层是HashMap ;HashMap的底层是(数组+链表+红黑树)

  • -------> 用一段代码来模拟一下 (数组+链表)

  • /**
     * 模拟一个HashSet底层(一个HashMap结构)
     */
    public class HashSetMode {
        public static void main(String[] args) {
    
            // 创建一个数组  数组类型是Node[]
            Node01[] table = new Node01[16];
            // 创建一个 join的节点
            Node01 join = new Node01("join", null);
            // 将join作为第一个节点放入 第二个数组内
            table[1] = join;
    
    
            // 将 节点 mark 悬挂在join后 形成链表
            Node01 mark = new Node01("mark", null);
            join.next = mark;
    
            for (Object o :table) {
                System.out.println(o);
            }
        }
    }
    
    class Node01{
        Object item;  // 存放内容
        Node01 next;   // 指向下一个节点
    
        public Node01(Object item, Node01 next) {
            this.item = item;
            this.next = next;
        }
    
        @Override
        public String toString() {
            return "Node01{" +
                    "item=" + item +
                    ", next=" + next +
                    '}';
        }
    }
    
    null
    Node01{item=join, next=Node01{item=mark, next=null}}
    null
    null
    .
    .
    .
    

(3)分析HashSet的添加元素底层是如何实现的(hash() + equals())
  • 结论:
HashSet底层是HashMap
添加一个元素时,先得到hash值 -会转成 -> 索引值
找到存储数据表table,看这个索引位置是否已经存放元素
如果没有,直接加入
如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则悬挂到后面
java8中,如果链表元素个数超过TREE_THRESHOLD(默认为8),并且table的大小 >= MIN_TREELFY-CAPACITY(默认为64),就进行树化(红黑树)
  • debug 测试代码

    public static void main(String[] args) {
            HashSet hashSet = new HashSet();
    
            hashSet.add("java");
            hashSet.add("php");
            hashSet.add("java");
    
            System.out.println(hashSet);
    
        }
    
    1. 执行 HashSet

       public HashSet() {
              map = new HashMap<>();
          }
      
    2. 执行add() 方法

      public boolean add(E e) {   // e: "java"
              return map.put(e, PRESENT)==null;  // e: "java"  map: "{}"
          }
      // 这里的 PRESENT 来自HashSet的 Static final 类型的一个对象,作用是占位,为了HashSet能够使用HashMap 
      
    3. 追进 执行put()方法

       public V put(K key, V value) {  // key:"java"   value = PRESENT 不变
              return putVal(hash(key), key, value, false, true);
          }
      
    4. 追进 看hash(key) ,是按照这个算法得到key对应的hash值

       static final int hash(Object key) {
              int h;
              return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
          }
      
    5. 返回 追进 putVal()

      1. 这里的 table 是HashMap的一个属性,是一个Node数组
      2. 第一次执行add 的情况
        final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                         boolean evict) {
              Node<K,V>[] tab; Node<K,V> p; int n, i;
            
      // 1.table = null 执行进入 ;if语句表示 如果当前的table为 null 或者大小为 0,就第一次扩容大小为16
              if ((tab = table) == null || (n = tab.length) == 0)  
                  n = (tab = resize()).length; //  resize 缓冲扩容机制 n: 16
            
      // 2.根据key得到的hash值,去计算该key应该存放到table表的哪个索引位置,并把这个位置的对象赋给p
      // 3. 判断p是否为null = 判断对应table索引位置有没有存放元素
      // 3. 如果p为null,表示还未放入元素,就创建一个Node(key,value)
              if ((p = tab[i = (n - 1) & hash]) == null)    // i: 3
                  tab[i] = newNode(hash, key, value, null); // key:"java"  value:PRESENT
              else {
                  Node<K,V> e; K k;
                  if (p.hash == hash &&  // p指向的是当前索引位置对应的最后一个元素;判断与预添加的key的hash是否相同
                      ((k = p.key) == key || (key != null && key.equals(k)))) // 同一个对象或内容相同
                      e = p;
                  
      // 判断p 是不是红黑树 
      // 如果是红黑树 就调用 putTreeVal();进行添加
                  else if (p instanceof TreeNode) 
                      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                  else {
      // 如果对应索引的table 已经形成了一个链表;就用这个for循环去判断
      // 依次和链表的每一个元素比较,如果都不相同,直接悬挂到最后面,否则 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);    // 当前链表如果到达8 就进行树化;treeifyBin()内要求table>= 64 才会树化,不然只会先扩容
                              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 增加的条件: 只要成功加入了一个节点 不管是加入table内还是Node内都满足条件,从而触发扩容
                  resize();  // 扩容
              afterNodeInsertion(evict);
              return null;
          }
      
      // resize()方法 第一次进入的处理  标示上面的 第二步
      // 扩容机制: 0.75 x 16 = 12 
      // 当使用12个空间的时候,就开始缓存=此时开始扩容,不等到16个空间全部使用完再扩容
      
      else {               // zero initial threshold signifies using defaults
                  newCap = DEFAULT_INITIAL_CAPACITY;
                  newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 
      // DEFAULT_LOAD_FACTOR(加载因子) =0.75  DEFAULT_INITIAL_CAPACITY = 1 >> 4 =16
              }
      

LinkedHashSet
  1. 为HashSet的一个子类
  2. 底层是 LinkedHashMap,底层维护的是一个(数组table+双向链表)
  3. 根据元素的Hashcode值,来决定元素的存储位置。同时使用链表维护元素的次序,使得元素的输出与插入次序一致。(加入次序和取出次序一致)
  4. 不允许添加重复的元素
  • 补充说明:

    1. 在LinkedHashSet 中维护了一个hash表和一个双向链表(LinkedHashSet有 head 和 tail)
    2. 每个节点有pre 和 next 属性,这样形成一个双向链表
    3. 在添加一个元素时,先求hash值,再求索引。确定该元素再hashtable的位置,然后将添加的元素添加到双向链表(如果已经存在不添加,原则和hashSet一样)
    4. 这样就实现了遍历LinkedHashSet时,插入顺序和输出顺序一致
  • 扩容机制:

添加第一次时,直接将table数组 扩容到 16 ,存放的节点类型是 LinkedHash E n t r y , 数 组 是 H a s h M a p Entry ,数组是 HashMap Entry,HashMapNode[]

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值