Set接口基本介绍

本文详细介绍了Set接口的基础概念,重点剖析了HashSet和LinkedHashSet的区别,包括元素的无序性、重复元素处理、添加方法、遍历方式以及底层数据结构(HashMap和双向链表)。
摘要由CSDN通过智能技术生成

Set接口基本介绍

  1. 无序(添加和取出的顺序不一致),没有索引
  2. 不允许重复元素,所以最多包含一个null
  3. JDK API中Set接口的实现类有:

image-20230523211951396

Set接口的常用方法

和List接口一样,Set接口也是Collection的子接口,因此,常用方法和Collection接口一样

下面以Set接口的实现类HashSet来讲解Set接口的方法

HashSet set = new HashSet();
set.add("john");
set.add("lucy");
set.add("john"); //重复
set.add("jack");
set.add(null);
set.add(null); //再次添加null

System.out.println("set=" + set); //set=[null, john, lucy, jack]

可以看出:

  1. Set 接口的实现类的对象(Set接口对象),不能存放重复的元素,可以添加一个null
  2. Set接口对象存放数据是无序的(即添加的顺序和取出的顺序不一致)
  3. 取出的顺序虽然不是添加的顺序,但是固定的

Set接口的遍历方法

同Collection的遍历方法一致,因为Set接口是Collection接口的子接口

  1. 可以使用迭代器

    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
      Object obj = iterator.next();
      System.out.println("obj=" + obj);
    }
    
  2. 增强for

    System.out.println("=======增强for========");
    for (Object o : set) {
        System.out.println("o=" + o);
    }
    
  • 不能使用索引的方式来获取

Set接口类——HashSet

  1. HashSet实现了Set接口

  2. HashSet实际上是HashMap,看下源码:

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

  4. HashSet不保证元素是有序的,取决于hash值,再确定索引的结果 (即,不保证存放元素的顺序和取出顺序一致)

  5. 不能有重复元素/对象,在前面Set 接口使用已经讲过

注意:

  1. 在执行add方法后,会返回一个boolean值,如果添加成功,返回 true,否则返回false

  2. HashSet不能添加相同的元素/数据?

    set.add("lucy"); //添加成功
    set.add("lucy"); //加入不了
    
    set.add(new Dog("tom")); //OK
    set.add(new Dog("tom")); //OK
    
HashSet底层源码机制说明
  • 分析HashSet的底层是HashMap,HashMap底层是(数组+链表+红黑树)
  • 分析HashSet的添加元素底层是如何实现的
HashSet hashSet = new HashSet();
hashSet.add("java");
hashSet.add("php");
hashSet.add("java");
System.out.println("set=" + hashSet);  

源码解读:

  1. 执行HashSet()

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

    public boolean add(E e) { //e = "java"
      return map.put(e, PRESENT)==null; //(static) PRESENT = new Object();
    }
    
  3. 执行put(),该方法会执行hash(key),得到key对应的hash值,算法是 h = key.hashCode() ^ (h >>> 16)

    public V put(K key, V value) { //Key = "java" value = PRESENT 共享
      return putVal(hash(key), key, value, false, true);
    }
    
  4. 执行putVal方法

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i; //定义了辅助变量
    

    table就是HashMap的一个属性,类型是Node[]

    下面的if语句,表示如果当前table是null 或者大小为0,就是第一次扩容,到16个空间

    if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
    
    1. 根据key,得到的hash,去计算该key应该存放到table表的哪个索引位置,并把这个位置的对象,赋给p
    2. 判断p是否为null
      1. 如果p为null,表示还没有存放元素,就创建一个Node (key="java", value=PRESENT)
      2. 就放在该位置 tab[i] = newNode(hash, key, value, null)
    if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //一个开发技巧提示:在需要局部变量(辅助变量)时候,再创建
            HashMap.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是不是一颗红黑树,如果是一颗红黑树就调用putTreeVal,来进行添加

    如果table对应索引位置,已经是一个链表,就是用for循环比较

    1. 依次和该链表的每一个元素比较后,都不相同,则加入到该链表的最后

      注意把元素添加到链表后,立即判断,该链表是否 已经达到8个结点,到达后就调用treeifyBin),对当前这个链表进行树化(转成红黑树)

      在转为红黑树时,还要再进行判断,判断条件

      if (tab == null || (n = tab.length) < MIN_TREEIFY_ACPacity(64))

      resize();

      如果上面的条件不成立,先table扩容

      只有上面的条件都成立时,才能转成红黑树

    2. 依次和该链表的每一个对象的比较过程中,如果有相同的情况,就直接break

    				else if (p instanceof HashMap.TreeNode)
                e = ((HashMap.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;
         //size 就是我们每加入一个结点 Node(k,v,h,next), size++
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    

image-20230524182110298

总结:

  1. HashSet的底层是 HashMap

  2. 添加一个元素时,先得到hash值-会转成–> 索引

  3. 转到储存数据表table,看到这个索引位置是否已经存放有元素

  4. 如果没有,直接加入

  5. 如果有,调用equals 比较(能重写),如果相同就放弃添加,如果不相同,则添加到最后

  6. 在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)

  • 分析HashSet的扩容和转成红黑树机制
  1. HashSet底层是HashMap,第一次添加时, table 数组扩容到 16, 临界值(threshold)是 16*加载因子(loadFactor)是0.75 = 12

    (当我们向hashSet增加了一个元素,-> Node -> 加入table,就算是增加了一个size++)

  2. 如果table 数组使用了临界值 12, 就会扩容到 16*2 = 32,新的临界值就是32*0.75 = 24,依次类推

  3. 在Java8中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制

Set接口实现类——LinkedHashSet

LinkedHashSet的全面说明
  1. LinkedHashSet 是 HashSet 的子类

  2. LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个数组 + 双向链表

  3. LinkedHashSet 根据元素的 hashSet 值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的

  4. LinkedHashSet 不允许插入重复元素

说明:

  1. 在LinkedHashSet 中维护了一个hash表和双向链表(LinkedHashSet 有 head 和 tail)

  2. 每一个结点有 pre 和 next属性,这样就可以形成双向链表

  3. 在添加一个元素时,先求hash值,再求索引,确定该元素在hashtable的位置,然后将添加的元素加入到 双向链表(如果已经存在,不添加) [原理和hashset一样]

    tail.next = newElement //简单指定
    newElement.pre = tail;
    tail = newElement;
    
  4. 这样的话,我们遍历LinkedHashSet 也能确保插入顺序和遍历顺序一致

image-20230524214520631

  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值