Set集合及源码分析

1. Set接口基本介绍

  1. 无序(添加和取出的顺序不一致),没有索引
  2. 不允许重复元素,所以最多包含一个null
public static void main(String[] args) {
        HashSet<Object> set = new HashSet<>();
        set.add(1);
        set.add(1);
        set.add("zjh");
        set.add("zjh");
        set.add("jxj");
        set.add("jxj");
        set.add(null);
        set.add(null);
        System.out.println(set);
    }

请添加图片描述

注意:这里说的无序,是输出的时候与添加的顺序不同,但是一直重复输出的话,输出的顺序是一样的。原因会在讲解源码的时候进行分析

1.1 Set接口的遍历方式

  1. 可以使用迭代器
  2. 增强for循环

2. HashSet

  1. HashSet实现了Set接口
  2. HashSet实际上是HashMap
    请添加图片描述
  3. 可以存放null值,但是只能有1个
  4. HashSet不保证元素是有序的,取决于hash后,在确定索引的结果
  5. 不能有重复元素/对象

2.1 HashSet的底层机制

请添加图片描述

HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树)

如上图,数组的每个位置都可以形成一个链表(链表是由节点组成,节点用来存储数据和指向下一个节点,从而形成链表,如果某个链表和总的数组的长度达到某个条件的时候,会将数组的某一位置的链表转换为红黑树)

2.2 HashSet的源码分析

2.2.1 结论

  1. HashSet的底层是HashMap
  2. 添加一个元素的时候,先得到hash值,然后转换为索引值(索引值就是确定是放在数组的哪个位置)
  3. 找到存储数据表table,看这个索引位置是否已经存放数据
  4. 如果没有则直接加入
  5. 如果有就调用equals比较,如果相同就放弃添加,如果不同就添加到最后(就是链表的形式)
  6. 在java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8)并且table(就是数组)的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会将链表长度超过8的链表进行树化(即转换为红黑树)

2.2.2 源码分析

测试代码

public class SetMethod {
    public static void main(String[] args) {
        HashSet<Object> set = new HashSet<>();
        set.add("java");
        set.add("php");
        set.add("java");
        System.out.println(set);
    }
}

结果
请添加图片描述
源码分析(在new HashSet()出打上断点)

首先进入无参构造器(可以看到其实底层是HashMap)
请添加图片描述
之后就进入了add()方法
请添加图片描述
请添加图片描述
PRESENT是一个Object类,接下来进入put()方法
请添加图片描述
上图可以发现put()方法里面是key和value,key就是我们存放的“java”字符串的值,value就是刚刚的PRESENT,即那个Object类,其实这个Object类没有什么作用,就是为了占位,能够使得HashSet存放时能够使用Map的key-value的形式。不管你执行多少次put()方法,这个Object一直不变。

接下来首先查看hash()方法
请添加图片描述
这里就是判断key是否为空,现在的这个key就是“java”,如果key为空则返回0,反之则返回对应的hash值。注意这个hash值不是hashcode

接下来进入putVal()方法

/**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @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; // 这些都是辅助变量
        if ((tab = table) == null || (n = tab.length) == 0) // 首先得明白,HashSet本质上是HashMap,所以本行代码中,table是HashMap中的table,这个table就是存放Node节点的数组,刚刚进来的时候,这个table为空
            n = (tab = resize()).length; // 如果tab为空或者长度为0,就执行这条语句 最主要的就是这个resize()方法,在下面进行介绍
        if ((p = tab[i = (n - 1) & hash]) == null) // 到这一步就是来确定你这个key对应的hash值应该存放的数组的位置是否为空
            tab[i] = newNode(hash, key, value, null); // 如果为空,则直接赋值,注意这个key就是我们存放的值,value就是那个Object对象,存放hash的目的就是为了后面的链表链接做准备 这个null的位置就是next指针,就是链表的next指向
        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(),就是扩容
            resize();
        afterNodeInsertion(evict); //这个方法是HashMap留给它的子类的方法,里面是空的,例如留给LinkedHashMap去实现
        return null;
    }

上述代码就是最重要的代码里,解释已经写到了上述代码里。

resize()方法

/**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length; // 这里table是null,即oldTab为null
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY; // 就进入到这里,赋予newCap一个常量,这个常量的值是16 这个就是默认初始化数组的容量大小
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // DEFAULT_LOAD_FACTOR这是默认加载因子,默认为0.75 DEFAULT_INITIAL_CAPACITY就是默认初始容量,newThr就是这两个相乘,这个newThr就是临界值,这是什么意思呢,就是如果你的数组达到了这个临界值,就是12,这个临界值会不断地变化的,因为随着你的容量的变化,你的临界值是不断会进行变化。如果你的容量达到临界值就会进行扩容,他就不会等到满了才会扩容,达到临界值就会进行扩容
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr; // 走到这里,threshold来记录临界值,刚刚开始是12
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 这里就开始创建长度为16的Node数组了
        table = newTab; // 把创建的长度为16的Node数组赋值给table
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab; // 最终就是返回newTab,现在就是长度为16的数组,因为这是刚刚开始创建的HashSet,刚刚开始add操作
    }

接下来就进入第二次add方法

/**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @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;
        if ((tab = table) == null || (n = tab.length) == 0) // 直接进入到这里,现在因为是第二次add了,table已经不为空,长度也变成了16
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null) // 这里就是根据key的hash来判断在table的位置,此时“php"对应的结果应该存放在数组索引位置为9的位置
            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;
    }

接下来开始第三次add()方法

 /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @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;
        if ((tab = table) == null || (n = tab.length) == 0) // 进入到这里,条件不成立,不会进去
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null) // 也不成立,因为之前已经存放过"java"了
            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)))) // 这个p就是当前索引位置的对象,如果 1.当前索引位置的第一个元素的hash和目前准备存放的值的hash值相同且索引位置的key就是目前要存放得到key是同一个对象 或者 2. 当前索引位置的第一个元素的hash和目前准备存放的值的hash值相同且(当前存放的key不为空且key.equals(索引位置存放的key)为true),只要满足上述条件,就将索引位置的节点赋值给e,其实就是认为不能往里面添加,就认为已经是重复了
                e = p;
            else if (p instanceof TreeNode)// 现在的这个else if和下面的else在本次add方法中已经不会触发了,现在就是进行一个解释说明这个else if和else的代码说明  这个else if就是判断这个p是不是红黑树,因为这个p他是数组索引位置的节点,通过这个节点来判断这个节点是不是红黑树
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 如果是一颗红黑树就调用putTreeVal来进行添加
            else {
                for (int binCount = 0; ; ++binCount) { // 这个for循环没有终止条件,要想跳出for循环,就只有下面的break生效
                    if ((e = p.next) == null) { // 这里就是获得索引位置的节点的下一个节点赋值给e 如果e为空,就添加节点到链表后面
                        p.next = newNode(hash, key, value, null); // 这里就是添加
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // 这里就是如果一直循环下去,如果当前索引位置的链表长度以及到达了临界值了,就把链表进行红黑树化 TREEIFY_THRESHOLD的值是8 因为我们的binCount 是从0开始的到7的时候,其实就已经有8个节点了,就要开始树化 注意,这里进行树化后,在进入树化的方法里面,还会判断数组长度是否小于64,如果小于64,则不会进行树化,只会进行数组扩容
                            treeifyBin(tab, hash); // 红黑树化
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) // 发现添加的值和链表中的值一样,立马就break,走掉,不添加
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key // 就到这里,
                V oldValue = e.value; // e的value就是那个Object对象
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue; // 返回这个Object对象,返回到上层进行判断,最后add方法的结果就是false,所以就添加失败
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

2.3 HashSet的扩容机制

  1. HashSet底层是HashMap,第一次添加时,table扩容到16,临界值是 数组容量*加载因子 加载因子是0.75
  2. 如果table数组包含链表使用长度达到了临界值就会进行扩容(例如索引为1的地方有长度为8的链表,索引为2的地方有长度为8的链表,这两处的长度加起来达到16,也会进行扩容),新的容量就是 数组容量*2 新的临界值也会进行改变,但是加载因子不会进行改变
  3. jdk8中,如果数组的长度达到64,且有链表的长度达到了8就会进行红黑树化,如果不满足,还是采取数组扩容的形式

3. LinkedHashSet

  1. LinkedHashSet是HashSet的子类
  2. LinkedHashSet底层是一个LinkedHashMap,底层维护了一个 数组+双向链表
  3. LinkedHashSet根据元素的hashcode值来决定元素存储的位置,同时使用链表来维护元素的次序,这使得元素看起来是以插入顺序进行保存的
  4. LinkedHashSet不允许添加重复的元素

代码案例

public class LinkedHashSetResource {
    public static void main(String[] args) {
        LinkedHashSet<Object> set = new LinkedHashSet<>();
        set.add(new String("AA"));
        set.add(456);
        set.add(456);
        set.add(new Customer("刘",100));
        set.add(123);
        set.add("HSP");
    }
}

模拟底层图
请添加图片描述

3.1 LinkedHashSet底层源码解析

案例代码

public class LinkedHashSetResource {
    public static void main(String[] args) {
        LinkedHashSet<Object> set = new LinkedHashSet<>();
        set.add(new String("AA"));
        set.add(456);
        set.add(456);
        set.add(new Customer("刘",100));
        set.add(123);
        set.add("HSP");
    }
}

说明:

  1. 在LinkedHashSet中维护了一个hash表和双向链表(LinkedHashSet有head头和tail尾)
  2. 每个节点有before和after属性,这样可以形成双向链表
  3. 在添加一个元素的时候,先求hash值,再求索引,确定该元素再table中的位置,然后将添加到元素添加到双向链表(添加的原则和HashSet一样)
  4. 遍历LinkedHashSet,其插入顺序和遍历顺序一致

源码分析(断点在new LinkedHashSet())

请添加图片描述
请添加图片描述
可以看到底层调用LinkedHashMap,初始化容量是16,加载因子是0.75
请添加图片描述
LinkedHashSet的底层结构如上图

接下来进入add方法
请添加图片描述

请添加图片描述

/**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @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;
        if ((tab = table) == null || (n = tab.length) == 0)
            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;
    }

请添加图片描述

请添加图片描述

其实可以大致看到和HashSet的底层几乎一样,就是存放的节点是Entry
请添加图片描述
Entry是一个内部类,继承了Node。

总结:

  1. LinkedHashSet和HashSet的区别是链表的区别和节点的区别(大致上)
  2. Set底层是Map
  3. Set存放的值其实是存放在key中(key-value结构),value中只是用一个Object来进行占位
  4. 为什么Set的存放的值是存放在key中,因为其key-value结构中,key具有不可重复性
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值