HashSet底层机制说明

HashSet的底层添加机制

HashSet不允许列表中存在相同的值并且里面的数据元素无序

结论

  1. HashSet的底层是HashMap

  2. 添加一个元素时,会先得到Hash值(HashCode方法) ,对Hash值进行会转成->索引值,也就是要存放在HashTable中的位置号。

  3. 找到存储数据表table,看这个索引位置是否已经存放的有数据‘

  4. 若是没有数据,则直接在这个索引上加入新数据

  5. 若是有数据,则调用equals方法比较,如果相同,就放弃添加;如果不相同则添加到链表最后。

  6. 在Java8中,如果一条链表的元素个数到达TREELFY_THRESHOLD(该值默认为8),并且

    table的大小默认>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)

  7. 如果链表的元素个数到达了TREELFY_THRESHOLD但是table的大小没有到达MIN_TREELFY_CAPACITY,则他会把table进行扩容

简单说就是

  1. 先获取元素的hash值(hashCode方法获取)
  2. 对hash值进行运算,得出一个索引值即为要存放在hash表中的位置号
  3. 如果该位置上没有其他元素,则直接存放
  4. 如果该位置上已经有其他元素,则需要进行equals判断,如果相同则不再添加。如果不相同,则以链表的形式添加。
  5. 关于equals方法,每一个类都会有自己的equals方法,比较String中重写equals方法来比较内容是否相同

底层源码

接下来将通过debug操作深入底层

###第一次添加

HashSet set = new HashSet;
set.add("java");
set.add("php");
set.add("java");//这个是故意的,事实上不会添加。
System.out.println("set="+set);
  1. HashSet set = new HashSet;下断点进行debug操作,此时它会调用HashSet的构造器HashSet()

    Code位置:HashSet.java

public HashSet(){
    map = new HashMap<>();
}

此时可以发现HashSet的底层走的是HashMap。

  1. 接下来运行到达set.add("java"),此时它会调用并执行add方法

    Code位置: HashSet.java

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

这里可以看见它是通过调用put方法,并判断其是否为null来确定是否添加这个数据。 断点进行下一步将会到达HashMap内的put方法。

Code位置:HashMap.java

public V put(K key, V value) {// key: "java"   value:Object@550
    return putVal(hash(key), key, value, false, true);
    //key: "java"  value:  Object@550
}

PS:value的值来自于PRESENTPRESENT是定义在HashSet.java的属性,定义语句如下:

private static final Object PRESENT = new Object();

可以知道这是一个final类型的静态对象,但是它实际上没什么意义,它在这里主要起到一个占位的目的。就是为了让HashSet使用到HashMap,它的value统一放的都是一个Object。总之key的值变化,但value的值传进来的始终是PRESENT

可以看到put方法返回putVal方法返回的值putVal方法会执行hash(key),得到key对应的hash值。

接下来分析putVal方法中接收的值都是些什么东西

  1. hash(key)

    这个方法是计算hash值的,最终返回一个key的hash值,hash值可以确定数据在数据表中的位置

    Code位置:HashMap.java

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode) ^ (h >>> 16); // key: "java"
        
        /**
        	1. 三目运算符
        	   如果key不为null,则按照(h = key.hashCode) ^ (h >>> 16)这个算法得到key的hash值
        	2. >>>是无符号右移的意思,这里无符号右移了16位
        	3. ^是按位亦或的意思
        	4. 这个右移 16 位再异或的操作是为了让 hashCode 的高位参与到哈希值的计算中,从而增加哈希值的分            布性,减少哈希冲突的概率
        */
    }
    

    注意,这个hash值并不完全等价key的HashCode,因为它在此基础上进行了计算。

  2. 再进行下一步,此时会运行到 putVal的底层源码

Code位于 :HashMap.java

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict){
    //hash: 3254803 key: "java" value: Object@550  onlyIfAbsent: false  evict: true
    
  	Node<K,V>[] tab; Node<K,V> p; int n, i;  //1. 定义了用于辅助的变量
  	if((tab = table) == null || (n = tab.length) == 0) 
         n = (tab = resize()).length;
    	//2.无论如何,执行了resize后,table事实上已经变成了大小为16的node数组了 
    	// 这块语句表示:如果当前table是null或者大小== 0 ,就是第一次扩容,直接扩容16个
    if((p = tab[i = (n - 1) & hash]) == null)
        //3.计算key(key:"java")对应的hash值来决定key应该在tab表中的哪一个索引位置去存放,并且把这个位置的对象赋给辅助变量p。
        //判断p是否为空,如果p为空则表示该索引位置没有存放过元素,则执行newNode在tab[i]43创建一个node
        tab[i] = newNode(hash, key, value, null);
		// newNode接受4个变量,hash是key的hash值、key:"java"、value=PRESENT,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 >= TREEIF_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) {
            V oldValue = e.value;
            if(!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;//记录修改次数
    if(++size > threshold)
        //size初始化为0,执行完必后增加一次,这里是判断当前数组元素的数量是否超过阈值
        //超过了就执行resize方法扩容。
        resize();
    afterNodeInsertion(evict);
   //这个方法是HashMap留给子类例如LinkedHashMap去实现再做操作的,对于HashMap来说这个方法是空方法,可忽略
   //子类可以去重写这个方法来实现例如有序链表之类的操作    
    return null;
   //返回空代表成功
}

//最后java添加在索引3的位置,这是HashSet的主要原因之一

上面涉及到到的方法

  1. resize()

    Code位置:HashMap.java

        final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;//1. 这个table在HashMap定义了,初始化为null
            int oldCap = (oldTab == null) ? 0 : oldTab.length;//2. 判断为null,给oldCap赋值为0
            int oldThr = threshold;//threshold是HashMap的属性,int类型初始化为0
            int newCap, newThr = 0;
            if (oldCap > 0) { //oldCap = 0,进入else哪里
                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;
                //新的capacity大小,就是数组开辟了多大的空间
         		// DEFAULT_INITIAL_CAPACITY是定义在HashMap的常量,初始化为1 << 4;(即1*2*2*2*2 = 16)
                //ps:位左移相当于*2,移动几位乘几次							
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
                //新的threshold,新计算的临界值,即0.75 * 16 = 12,也就是说当capacity内的元素到达12个就			  进行扩容操作的准备。这个是一个加载因子的操作,一个缓冲层,防止大量数据使程序崩溃。
                //DEFAULT_LOAD_FACTOR,常量,0.75f 
            }
            if (newThr == 0) {
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
            threshold = newThr; //上面计算出了临界值,进行临界值数据的更新
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
            //创建一个新的Node数组,容量即为刚刚开辟的空间,然后将这个新node数组赋给table。
            //到了这里实际上已经将node数组扩容好了,到这里正常的流程基本结束,直接返回newTab
            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;
        }

  2. afterNodeInsertion(evict)

    void afterNodeInsertion(boolean evict) { }
    //空方法,这个是给子类去重写实现的比如LinkedHashMap
    

    重写后:

    Code位置:LinkedHashMap.java

    
        void afterNodeInsertion(boolean evict) { // possibly remove eldest
            LinkedHashMap.Entry<K,V> first;
            if (evict && (first = head) != null && removeEldestEntry(first)) {
                K key = first.key;
                removeNode(hash(key), key, null, false, true);
            }
        }
    

第二次添加

第二次添加和第一次添加存在不同,但都是依靠HashMap中的putVal方法进行添加,我们现在要添加"php"

所以此时key: "php",它的hash值也不一样,putVal会根据hash值进行与运算来给"php"进行分配索引

    /**
    	int hash = hash(key); key: "php"; value: PRESENT
    */
	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) 
            //因为table表已经存在,所以这里判断为false
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        	//通过hash的与运算得到了key的位置,判断为空后创建一个新的节点
        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;
    }
//最后"php"被添加到了索引9

第三此添加:添加重复数据,数据不添加

hashSet不允许同一个数据存在

    /**
    	key: "java" value: PRESENT
    */

	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;
        //同样因为表存在它判断为false
        if ((p = tab[i = (n - 1) & hash]) == null)
            //注意,因为在第一次添加时添加了“java”,再次添加"java"时一定会判断计算出来的索引不为空
            //因为这个java的hash值和第一个"java"的hash值相同
            //判断为假,进入else
            tab[i] = newNode(hash, key, value, null);
        else {
            // 接下来它分成了三种情况:
            
            Node<K,V> e; K k;// 这里是创建了两个辅助用的局部变量e和k
            
            if (p.hash == hash &&
                //在满足 准备加入key和p指向的Node节点的key是同一个对象(就是hash相同)的前提下 
                ((k = p.key) == key|| (key != null && key.equals(k))))
                //满足以下条件 
                // (1)准备加入的key 和 p 指向的Node节点的key是同一个对象
                //  (2) key不为空且p指向的Node节点的equals()方法和准备加入的key比较后相同
                e = p;
            	//将p赋给e
            else if (p instanceof TreeNode)
                //如果第一个if为false 判断是否是红黑树
                //如果是,就调用putTreeVal,进行添加
                //putTreeVal内带有大量与红黑树有关的复杂算法,暂且不追
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //以上两个情况都不满足,这个时候就会启用一个循环比较的机制
                //如果table对应的索引位置,已经是一个链表,就使用for循环比较
                //为什么是用for循环进行比较?因为索引内元素是一个链表,要进行链表的遍历一个个比较
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //假如遍历后没有发现链表内有相同的元素,并且下一个节点为null
                        //则创建一个新节点
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st(索引从-1开始)
                            //TREEIFY_THRESHOLD初始化为8
                            treeifyBin(tab, hash);
                        	//在转成红黑树时还进行一个判断,这个判断在treeifyBin的方法内
                        	//如果链表的节点超过9(即循环次数超过7),就将其红黑树化
                        	/
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    	//如果依次和该链表的每一个元素比较后有相同的,退出循环
                    
                    p = e;//注意:p在每一次遍历后都会改变指向的节点
                }
            }
            
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                //返回PRESENT,反正不返回空,就是失败。
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

treeifyBin(tab, hase)

Code位置: HashMap.java

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
    	//MIN_TREEIFY_CAPACITY的值初始化为64
    	//这里会进行判断,如果tab的长度小于64,则进行扩容操作,只有这个条件不成立时才进行红黑树化
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

底层后总结

扩容和红黑树是不同的机制

  1. HashSet的底层是hashMap,第一次添加时,table数组扩容到16,临界值(threshold) = 数组大小*加载因子(loadFactor是0.75)得到的,一开始为12,即threshold = 16 * 0.75。
  2. 如果table的数组使用到了临界值12就会扩容到16 * 2 = 32 ,此时临界值threshold就是32 * 0.75 = 24,剩下由此类推。
  3. 在Java8中,如果一条链表的元素个数到达了TREELFY_THRESHOLD默认是8,并且table大小 >=MIN_TREEIFY_CAPACITY(默认是64)就会进行树化(红黑树),否则仍然采用数组机制扩容。

哈希计算索引机制

由于链表添加的索引是通过计算hash值得到的,且在同一个索引下,如果数据不存在相同则直接挂在到该索引内的链表的后面,那么就可以这样干

  1. 当我们需要在链表上的一个索引挂载多个元素时,我们可以重写hashCode方法

    public class Main{
        public static void main(String[] args){
       		HashSet hashSet = new HashSet();
            for(int i = 0;i <= 12;i++){
                hashSet.add(new A(i));
            }
            System.out.println("hashSet= " + hashSet);
        }
    }
    
    class A {
        private int n;
        public A(int n){
            this.n = n;
        }
        
        @ Overridden
        public int hashCode() {
        	return 100;    
        }
        //这样A对象所有的hashCode都是一样的
    }
    
    
  • 15
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值