Java教程

HashSet源码分析(JDK1.8)

本文主要是介绍HashSet源码分析(JDK1.8),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

在这里插入图片描述
其中绿色代表接口,橘色代表的是实现类由结构图可以看来HashSet实现了Set接口,LinkedHashSet为HashSet的子类。
在这里插入图片描述

添加一个元素的时候会先得到hash值,会转化为索引值 找到存储数据表table,看这个索引位置是否存放有元素 如果没有直接加入、如果有就调用equals方法比较,如果相同,就放弃添加,如果不同就添加到最后。
java8中,如果一个链表的元素个数>=treeify_threshold(默认为8),并且table的大小>=64就会进行树化(红黑树) 如果table数组长度没有超过64就扩展table数组。

add()方法

根据以下代码进行分析。

 public static void main(String[] args) {
        HashSet  hs = new HashSet();
        hs.add("java");
        hs.add("c++");
        hs.add("java");
        System.out.println(hs)
    }
 HashSet  hs = new HashSet();

HashSet底层是HashMap
//源码 public HashSet() {
map = new HashMap<>();
}

继续深入HashMap可以看到

public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
其中有一个DEFAULT_LOAD_FACTOR为加载因子这个加载因子在HashMap中被定义
static final float DEFAULT_LOAD_FACTOR = 0.75f;

 hs.add("java");

add方法调用了map的put方法
public boolean add(E e) {
//判断put函数的返回值是否为null
return map.put(e, PRESENT)==null;
}

可以看到有一个PRESENT的参数,这个PRESENT在HashSet类里面被定义为一个常量:
private static final Object PRESENT = new Object();

因此在HashSet中调用add()方法的时候是将add里面的参数存放在key中的,而value值则是一个常量。(因此可以解释双列集合HashMap实现了单列集合HashSet)

继续追踪 map.put(e, PRESENT)函数
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
发现putVal函数里面有一个参数hash(key),进入到hash(key)里面
static final int hash(Object key) {
int h;
//将 hashCode 的高16位和 hashCode 进行异或(XOR)运算,得到最终的 hash 值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

!!!前方高能!!!

从hash(key)里面出来之后进入:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)

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;
    }

分析:
进入到putVal函数里面首先会判断table表是否为空,或者判断Node数组的长度是否为0,如果满足条件就会调用resize()函数为table数组去重新定义一个长度。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        /*判断table是不是为为null如果为null返回表长为0,否则返回表长*/
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//oldCap记录旧的容量
        int oldThr = threshold;//OldThr记录旧的阈值
        int newCap, newThr = 0;//分别记录新的容量和阈值
        if (oldCap > 0) {
        /*
        如果满足旧表的长度大于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; 
        }
        else if (oldThr > 0) 
            newCap = oldThr;
        else {    
        //否则就会为table表设置一个新的容量DEFAULT_INITIAL_CAPACITY为16           
            newCap = DEFAULT_INITIAL_CAPACITY;
       //新的阈值为加载因子*容量=12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        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;
        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;
    }

第一次进入到resize()函数里面table表为null,因此oldCap=0;
判断oldCap进入到
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR *DEFAULT_INITIAL_CAPACITY);
}
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
return newTab;
在else里面将newCap的值设置为DEFAULT_INITIAL_CAPACITY即16 ,newThr为 加载因子*容量=12。
再将newThr的值赋值给threshold(阈值),之后根据新的容量newThr去创建一个新的数组;将新的数组返回。

从resize()函数出来之后继续执行putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)函数
之后执行:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
分析:根据(n - 1) & hash计算当前的Node应该放到table表的哪个位置,并且判断当前位置有没有存放其他的值。由于这是第一次去存放因此这个位置一定是空的,所以就在当前位置去创建一个新的结点。

++modCount;//结点总数加一
//判断当前的总数size是否大于阈值,如果大于阈值则扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;

最终返回到:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
返回结果为true因此第一个元素"java"添加成功!!

hs.add("c++");

同理追加进去:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
再进入到map.put(e, PRESENT)函数
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
因为key值为"C++“不同于第一次添加的"java”,所以他们的hashCode()的值不一样因此调用hash(key)函数的值不一样,所以他们在HashMap中不会出现冲突,因此之后的执行过程与第一次添加"java"的过程一样。

  hs.add("java");

同理追加进去:
public boolean add(E e) { return map.put(e, PRESENT)==null;
再进入到map.put(e, PRESENT)函数
public V put(K key, V value)
{ return putVal(hash(key), key, value, false, true);}

此时因为第二次添加的值为"java",因此会产生hash碰撞
进入到:

else {
            Node<K,V> e; K k;
            /*判断传进来的hash与当前的hash值是否相同并且判断key是否相同,!!!
            key值的比较是通过== 或者 equals方法进行比较,具体的怎么才是key
            值相同需要自己去重写equals方法来进行定义*/
            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) {
                	//如果当前节点的下一个节点为NUll,那么就新创建一个节点挂到末尾。
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //如果结点个数大于或者等于8就调用treeifyBin(tab, hash)函数,可能将链表转化为红黑树。
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
                            treeifyBin(tab, hash);
                        break;
                    }
                    //或者在链表中找到了与这个结点key值一样的结点,那么就会添加结点失败退出
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

根据上面的代码可以看出进入到else语句中会遍历链表,采用的是for循环,这个for循环没有判断条件是个死循环,退出的条件是要么没有找到一个key值相同的就在链表的末尾去加上这个结点,要么是找到key值相同的就退出。
本案例是产生了冲突,并且当前链表中存在key值为"java"的结点,因此添加结点失败。

注意:

并不是链表结点个数大于或者等于8就直接被转化为红黑树,而是调用了 treeifyBin(Node<K,V>[] tab, int hash)函数,观察这个函数可以看到当table数组的长度小于MIN_TREEIFY_CAPACITY的时候不会转化为红黑树,而是将table数组进行扩容!

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();
        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);
        }
    }

图解:在链表上添加结点的过程
假设要添加一个节点"coco",计算其hash值与"apple"节点的hash相同即发生了冲突,但是链表上的key值都不相同的情况

在这里插入图片描述执行 p.next()!=null; p = e;
在这里插入图片描述
执行 p.next()!=null; p = e;在这里插入图片描述
执行 p.next()!=null; p = e;在这里插入图片描述
执行 p.next()!=null; 在这里插入图片描述
执行p.next()==null ; p.next = newNode(hash, key, value, null);
在这里插入图片描述
假设要添加一个结点"potato",计算hash值与"apple"结点的hash值相同即发生了冲突,链表上已经有key值为"potato"的结点。
在这里插入图片描述
执行p.next()!=null; p = e;
在这里插入图片描述链表中找到了与这个结点key值一样的结点,跳出循环,添加结点失败!
在这里插入图片描述

这篇关于HashSet源码分析(JDK1.8)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!