Java集合—Set(Collection子接口)及其子类(HashSet、LinkedHashSet)包括HashMap源码分析

Set接口是 Collection接口的子接口。
1、无序,即添加元素和去除元素的顺序不一致。
但是每次取出的顺序是一致的。
2、不允许重复元素,可以有null,但只能有一个。
3、实现类很多,主要介绍HashSet、LinkedHashSet 和 TreeSet。
常用方法
因为其为Collection的子接口,因此常用方法和Collection一致。
遍历方式
因为其是Collection的子接口,所以其遍历方法与Collection一致。
即:
  1. 使用迭代器Iterator
  2. 使用增强for

一、HashSet

  1. HashSet实现了Set接口
  2. HashSet底层上是HashMap,其底层为:数组+链表+红黑树
  3. 可以存放null,但是只能存放一个。
  4. HashSet不保证数据是有序的(即不保证存取顺序一致),取决于hash后,再确定索引的结果。
  5. 不能有重复的对象

源码

  • HashSet底层是HashMap
  • 添加一个元素时,先得到hash值,将其转变为索引值
  • 找到存储数据表table,判断该索引位置是否已经有存放的元素
    • 没有,则直接加入
    • 有,则调用 equals 比较,相同则放弃添加;不同,则添加到链表后面
  • 在Java8中,如果一条链表个数到达了TREEIFY_THRESHOLD(默认值为8),
           并且table>=MIN_TREEIFY_CAPACITY(默认64),就会转化成红黑树。
1、新建对象
    调用自身构造器,在构造器内调用HashMap()构造器。
    
  
2、add()添加对象
    执行add()方法,调用put()方法
    然后执行put()方法,调用hash()获取 key值 ,之后调用putVal()方法
    putVal()则是真的存放数据的方法。
①add()方法
    可见,add()方法直接调用 put()方法,传入两个参数,分别为key和value;
        key为我们add的数据;
        value为PRESENT,下面第二张图,可见其为一个静态的常量,为所用对象共有的
        因为其底层是HashMap,是键值对,其中key为HashSet要存放的数据,Value如果填Null,虽然节省空间,
        但是意味着无论是否添加成功,都返回Null,那么则无法判断是否添加成功,因此放了 PRESENT。
        可见, 返回null才是添加成功
    
    
②HashMap.put()方法:
    put()方法直接先调用hash()获取hash值,然后调用putVal()方法。
    
    hash()将我们 即将添加的值hashcode值 与 其hashcode无符号向右位移16位的值 按位异或。
    目的就是为了减少碰撞, 具体原因: h = key.hashCode()) ^ (h >>> 16)  
    
    hashcode()方法获取hash值:
        不同类不一样,一般都会重写。
    Object类:    
        Object的hashcode 方法是本地方法,也就是用 c 或 c++ 实现的,该方法直接返回对象的内存地址。
        注意其为native修饰。详情: Object顶级类(包含==和equals区别)
        
  
③HashMap.putval()方法:
源码前,先看:
    table为HashMap存放数据的数组,为Node类型的数组;
    
    Node为每个数据所存放的对象:
        hash:存放对象Key所对应的hash值
        key:存放数据对象
        value:每个对象都一样,就是①中所说的PRESENT,一个Object类型的常量对象。
        next:是下一个节点的引用。
    
    resize()数组扩容源码
final Node<K,V>[] resize() {
    //将table赋值给oldtab
    Node<K,V>[] oldTab = table;

    //oldcap存放旧table数组的大小
    //    如果旧数组为null,则oldcap为0
    //    如果旧数组不为null,则oldcap为其原来的大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;

    //oldthr存放扩容阈值,HashMap并不是数据满了才存放,而是达到阈值就进行扩容
    int oldThr = threshold;

    //newcap存放新的数组大小,newthr存放新的扩容阈值
    int newCap, newThr = 0;

    
    //oldcap大于0,即table扩容前不为null的情况
    if (oldCap > 0) {
        ...
    }

    //初始容量设置为阈值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;

    //table为null的情况
    //newcap赋值为DEFAULT_INITIAL_CAPACITY,
    //newthr赋值为(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)
    //DEFAULT_INITIAL_CAPACITY为默认初始容量16,定义:static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //DEFAULT_LOAD_FACTOR为默认缓存大小0.75,定义:static final float DEFAULT_LOAD_FACTOR = 0.75f;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    //修改newThr
    if (newThr == 0) {
        ...
    }

    //将newthr赋值给threshold,即缓存大小设置完毕
    threshold = newThr;

    @SuppressWarnings({"rawtypes","unchecked"})
    //因为table为数组,因此扩容需要新建立数组
    //然后将新数组赋给table
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;

    //如果旧数组table不为null,则需要将旧数组中的数据转移到新数组,具体先不研究
    if (oldTab != null) {
                . . .
    }
    
    //最后返回新的数组table
    return newTab;
}
    treeifyBin():判断对应链表是否需要树化
        先进行判断,如果数组长度小于MIN_TREEIFY_CAPACITY(默认64),则调用resize()进行扩容
        否则进行树化。
    
putVal()源码如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {

    //tab为table
    //n用来存放table长度;
    //p用来存放table中将要存放的位置的对象
    //i用来存放table中将要存放位置的下标
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    //将table赋给tab,判断是否为null或者判断length是否为0
    //若table不为空,则跳过该段代码,否则进入resize()方法
    //将长度赋给n
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

    //计算出待存放位置,即i位置的table[i]为null
    //1.将 n-1 和 传进来的hash 进行按位与,并将其赋值给 i
    //2.将tab[i],即按位与得到下标的位置在数组中的对象赋值给p
    //3.如果p为null,说明该位置还没有存放,则新建一个结点存放进去,结点信息在上面
    //4.然后该结点赋值给tab[i]
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    //计算出待存放位置不为空,即 待存储数据的hash值 与 之前存储过的数据hash值 一致,
    //是否存入新数据如下:
    else {
        //创建辅助变量
        Node<K,V> e; K k;
        //如果 p.hash即数组当前位置的链表的第一个元素的hash值 与 hash即待插入的新元素的hash值相同
        //且满足下面条件之一:(将p.key赋给k)
        //    k == key 即 第一个元素的key 和 带加入元素的key 是同一个对象
        //    key != null && key.equals(k) 即 key非空 且 key 和 kequals相同,即内容相同
        //则将 p 赋值给 e,p就是当前数组当前位置的链表的第一个元素
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //如果p是一颗红黑树,就以红黑树添加元素的方式进行比较并添加新的对象
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //如果不满足上述两种情况,即值不同,且p还不是红黑树
        //则当前p为一个链表
        //就进行循环
        else {
            for (int binCount = 0; ; ++binCount) {
                //遍历到了尾部,追加新节点到尾部
                //先判断p.next是否为null
                //如果为null则直接将新结点存入,并退出循环
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //如果新增加元素后 > TREEIFY_THRESHOLD(默认为8)-1=7,即有了8个结点,
                    //则调用treeifyBin()进行判断是否树化,方法在上面
                    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;
            }
        }
        //如果e不等于null,则说明原来存在与带加入节点hash值相同,但是value值不同节点
        //然后用新的value代替旧的value
        //则返回旧value
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //空方法,没实现,LinkedHashMap的时候有用到
            afterNodeAccess(e);
            return oldValue;
        }
    }

    //modCount即操作数+1
    ++modCount;

    //size即数组大小+1
    //+1之后的size如果大于了threshold,则需要扩容
    //注意,此处是size>threshold,而size每次增加是在加入一个结点之后;
    //意味着并不是数组table用到了threshold个,而是结点达到threshold个。
    if (++size > threshold)
        resize();

    //该函数在HashMap中是一个空函数
    //该函数由HashMap的子类实现,用来实现排序存放等。
    afterNodeInsertion(evict);
    
    //最后返回null为添加成功
    return null;
}

二、LinkedHashSet

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

源码

  • LinkedHashSet维护一个hash表和双向链表
    • 该类有两个属性head 和 tail ,用来表示头节点和尾节点
  • 每一个节点还有 pre 和 next 属性,用来形成双向链表
  • 添加一个元素时,先求hash值,再求索引,确定其在table中的位置
    • 如果没有重复,跟HashSet一样加入,之后再加入双向链表
    • 有重复则不加入
  • 因此,遍历时使用链表,就会呈现和添加顺序一样的遍历顺序了。
  • 存放的节点不是Node了,而是
1、构造器:
    可见构造器是直接调用父类的有参构造器,默认初始容量为16,阈值为0.75。
    注意, HashSet()中创建的对象是LinkedHashMap(),而不是HashMap()。
    
    
2、add()添加元素:
    与HashSet类似,也是直接调用LinkedHashMap的add()方法
    然后调用 HashMap 的put()方法,因为LinkedHashMap并未重写该方法
    再调用 HashMap 的putVal()方法
    
    
putVal()方法:
    与HashSet一样,但是在 newNode()时,使用的是LinkedHaspMap的方法:
        先创建一个Entry对象。
        然后调用父类构造方法构造Node。
            注意:Entry继承了HashMap的内部类Node
        再调用linkNodeLast()方法将该节点链接在链表当中。
    
    
    
    与HashSet不同的还有在最后 afterNodeInsertion(evict)是实现了的
    
    
  • 46
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值