【手撕Java集合】Set 集合常用类详解(附部分源码剖析~~~)

1. Set 集合概述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-04cosRHb-1644809107366)(./images/1629775651285.png)]

Set 集合的特点就是:无序,元素不可重复,至多只有一个null值

2. Set 集合常用子类

  • HashSet
    • (无序,唯一)基于 HashMap 实现的,底层采用 HashMap 来保存元素
  • LinkedHashSet
    • LinkedHashSetHashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。(有点类似于 LinkedHashMap 其内部是基于 HashMap 实现,不过还是有一点点区别的 )
  • TreeSet
    • (有序,唯一)红黑树(自平衡的排序二叉树)

以上三个类都是非线程同步

3. HashSet

类图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LR334OwS-1644809107369)(./images/1629776305999.png)]

  • 实现了Cloneable接口,可以克隆
  • 实现了Serializable接口,可以序列化、反序列化
  • 实现了Set接口,是Set的实现类之一
  • 实现了Collection接口,是Java Collections Framework成员之一
  • 实现了Iterable接口,可以使用for-each迭代(但不建议,因为无序)

HashSet 类注释:
在这里插入图片描述

从类注释来看,可以归纳 HashSet 的特点:

  • 实现 Set 接口
  • 不保证迭代顺序
  • 允许元素为 null
  • 底层实际上是一个HashMap实例(基于哈希表实现)
  • 非线程同步
  • 初始容量非常影响迭代性能

HashSet 是对 HashMap 的简单包装,对 HashSet 的函数调用都会转换成合适的 HashMap 方法,因此 HashSet 的实现非常简单,只有不到300行代码。

3.1 HashSet 的属性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wiYMEI2b-1644809107385)(./images/1629777022033.png)]

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;//序列化版本UID

    private transient HashMap<E,Object> map;//底层用来存储数据的真实容器map

    // Dummy value to associate with an Object in the backing Map
    //由于Set只使用到了HashMap的key,所以此处定义一个静态的常量Object类,来充当HashMap的value
    private static final Object PRESENT = new Object();
    
    ......
3.2 HashSet 的构造器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qyMM3V2w-1644809107388)(./images/1629777441554.png)]

构造器详解:

private transient HashMap<E,Object> map;
//默认构造器,使用HashMap的默认容量大小16和默认加载因子0.75初始化map,构造一个HashSet
public HashSet() {
    map = new HashMap<>();
}
//构造一个指定Collection参数的HashSet,这里不仅仅是Set,只要实现Collection接口的容器都可以
public HashSet(Collection<? extends E> c) {
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}
//明确初始容量和装载因子的构造器
public HashSet(int initialCapacity, float loadFactor) {
    map = new HashMap<>(initialCapacity, loadFactor);
}
//仅明确初始容量的构造器(装载因子默认0.75)
public HashSet(int initialCapacity) {
    map = new HashMap<>(initialCapacity);
}
//不对外公开的一个构造方法(默认default修饰),底层构造的是LinkedHashMap,dummy只是一个标示参数,无具体意义
HashSet( int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}

通过上面的源码,我们发现了HashSet就像是一些公司,它就对外接活儿,活儿接到了就直接扔给外包 HashMap 处理了。因为底层是通过 HashMap 实现的,这里简单提一嘴:

HashMap的数据存储是通过数组+链表/红黑树实现的,存储大概流程是通过hash函数计算在数组中存储的位置,如果该位置已经有值了,判断key是否相同,相同则覆盖,不相同则放到元素对应的链表中,如果链表长度大于8,就转化为红黑树,如果容量不够,则需扩容(注:这只是大致流程)。

只有最后一个构造方法有写区别,这里构造的是 LinkedHashMap,该方法不对外公开,实际上是提供给 LinkedHashSet 使用的,而第三个参数 dummy 是无意义的,只是为了区分其他构造方法。

3.3 add 方法

HashSet 的 add 方法时通过 HashMap 的 put 方法实现的,不过 HashMap 是 key-value 键值对,而 HashSet 是集合,那么是怎么存储的呢,我们看一下源码 :

private static final Object PRESENT = new Object();

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

看源码知道,HashSet 添加的元素是存放在 HashMap 的 key 位置上,而 value 取了默认常量 PRESENT,是一个Object 对象,至于 map 的 put 方法,下面是其大致工作流程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4tM4vfgQ-1644809107392)(./images/1629778587321.png)]

3.4 remove 方法

HashSet 的 remove 方法通过 HashMap 的 remove 方法来实现

//HashSet的remove方法,形参其实是key,也就是我们存入Set的值
public boolean remove(Object o) {
    return map.remove(o)==PRESENT;//如果删除key会返回其对应的value,只要存在这个key,它的value绝对是PRESENT,则结果为真,Set中不存在这个key的话自然为假
}

//map的remove方法
public V remove(Object key) {
    Node<K,V> e;
    //通过hash(key)找到元素在数组中的位置,再调用removeNode方法删除
    return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}

//真正进行删除元素的逻辑
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    //步骤1.需要先找到key所对应Node的准确位置,首先通过(n - 1) & hash找到数组对应位置上的第一个node
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        //1.1 如果这个node刚好key值相同,运气好,找到了
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        /**
         * 1.2 运气不好,在数组中找到的Node虽然hash相同了,但key值不同,很明显不对,我们需要遍历继续
         *     往下找;
         */
        else if ((e = p.next) != null) {
            //1.2.1 如果是TreeNode类型,说明HashMap当前是通过数组+红黑树来实现存储的,遍历红黑树找到对应node
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                //1.2.2 如果是链表,遍历链表找到对应node
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        //通过前面的步骤1找到了对应的Node,现在我们就需要删除它了
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            /**
             * 如果是TreeNode类型,删除方法是通过红黑树节点删除实现的
             */
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            /** 
             * 如果是链表的情况,当找到的节点就是数组hash位置的第一个元素,那么该元素删除后,直接将数组
             * 第一个位置的引用指向链表的下一个即可
             */
            else if (node == p)
                tab[index] = node.next;
            /**
             * 如果找到的本来就是链表上的节点,也简单,将待删除节点的上一个节点的next指向待删除节点的
             * next,隔离开待删除节点即可
             */
            else
                p.next = node.next;
            ++modCount;
            --size;
            //删除后可能存在存储结构的调整,可参考【LinkedHashMap如何保证顺序性】中remove方法
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}
3.5 遍历

HashSet作为集合,有多种遍历方法,如普通for循环,增强for循环,迭代器,我们通过迭代器遍历来看一下 :

public static void main(String[] args) {
    HashSet<String> setString = new HashSet<> ();
    setString.add("星期一");
    setString.add("星期二");
    setString.add("星期三");
    setString.add("星期四");
    setString.add("星期五");

    Iterator it = setString.iterator();
    while (it.hasNext()) {
        System.out.println(it.next());
    }
}

打印结果:

星期二
星期三
星期四
星期五
星期一

HashSet 是通过 HashMap 来实现的,HashMap 通过 hash(key) 来确定存储的位置,是不具备存储顺序性的,因此 HashSet 遍历出的元素也并非按照插入的顺序。

通过 for-each 遍历 HashSet

第一步: 根据 toArray() 获取 HashSet 的元素集合对应的数组。

第二步: 遍历数组,获取各个元素。

//假设set是HashSet对象,并且set中元素是String类型
String[] arr = (String[])set.toArray(new String[0]);
for (String str:arr)
   System.out.printf("for each : %s\n", str);
3.6 是否包含

利用 HashMap 的 containsKey 方法实现 contains 方法

//检查是否包含元素o
public boolean contains(Object o) {
   return map.containsKey(o);
}
      
/**
 * 检查是否包含指定集合中所有元素,该方法在AbstractCollection中
 */
public boolean containsAll(Collection<?> c) {
   // 取得集合c的迭代器Iterator
   Iterator<?> e = c.iterator();
   // 遍历迭代器,只要集合c中有一个元素不属于当前HashSet,则返回false
   while (e.hasNext())
     if (!contains(e.next()))
          return false;
   return true;
}

由HashMap 基于 hash 表实现,hash 表实现的容器最重要的一点就是可以快速存取,那么 HashSet 对于 contains 方法,利用 HashMap 的 containsKey 方法,效率是非常之快的。

3.7 总结
  • HashSet 中元素不可重复
  • 允许元素为 null
  • 不保证迭代顺序
  • 底层实际上是一个 HashMap 实例(基于哈希表实现)
  • 非线程同步
  • 初始容量非常影响迭代性能

4. LinkedHashSet

类图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ebDVwnG5-1644809107398)(./images/1636779582925.png)]

LinkedHashSet 类注释:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MBvip2rl-1644809107401)(./images/1636779733357.png)]

总结:

  • 迭代是有序的,元素插入和取出顺序满足 FIFO
  • 允许为 null
  • 底层实际上是一个 HashMap + 双向链表实例(其实就是 LinkedHashMap)
  • 非同步
  • 性能比 HashSet 稍差,因为要维护一个双向链表
  • 初始容量与迭代无关,LinkedHashSet 迭代的是双向链表

5. TreeSet

类图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3ULz8kNm-1644809107402)(./images/1636780181489.png)]

TreeSet 的类注释:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uUOIVPn2-1644809107404)(./images/1636780321519.png)]

总结:

  • 实现 NavigableSet 接口
  • 可以实现排序功能
  • 底层实际上是一个 TreeMap 实例(红黑树)
  • 非同步

属性和构造器:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xw8kdcXE-1644809107405)(./images/1636780416123.png)]

实际上底层是一个 TreeMap 实例,其中的 map 的每个 value 放的是 Object 对象,实际上是用 key 来存储 TreeSet 的值。

6. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同

  • HashSetLinkedHashSetTreeSet 都是 Set 接口的实现类,都能保证元素唯一,并且都不是线程安全的。
  • HashSetLinkedHashSetTreeSet 的主要区别在于底层数据结构不同。HashSet 的底层数据结构是哈希表(基于 HashMap 实现)。LinkedHashSet 的底层数据结构是双向链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。
  • 底层数据结构不同又导致这三者的应用场景不同。HashSet 用于不需要保证元素插入和取出顺序的场景,LinkHashSet 用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet 用于支持对元素自定义排序规则的场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kaho Wang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值