Java初学之详述HashSet中的add方法

add方法

add方法,向HashSet集合中添加元素。

我们先看一段代码。

package add;

import java.util.HashSet;

public class Test {

	public static void main(String[] args) {
		HashSet list=new HashSet<>();
		String name="Tom";
		list.add(name);
	}

}
 
  

显而易见,我们创建了泛型为Object类型的list集合,并添加了字符串类型的元素“Tom”。

我们对add方法按ctrl键,查看add方法的底层源代码,结果如下——

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

我们可以看出,e就是我们传入的name,而E指代e 的数据类型,类似于方法传参时的(int a)。

我们传入e,但是还调用了map接口的put方法——

put(K key, V value) 将键(key)/值(value)映射存放到Map集合中

我们知道,在HashMap中key即键是不能重复的,否则覆盖已有的key对相应的值 。

所以HashSet添加的元素也不允许重复。显然key就是我们输入的值,value是add方法之中默认的PRESENT,因为是大写字母,显然是个常量。

继续分析,add方法返回的是一个boolean类型的值,map调用了put方法,其中e是key,而value被设置成了常量。

这行代码的意思是,如果put方法的返回值为null,那么就返回true。

我们继续点开HashMap中的put方法,得到代码——

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

返回的是一个putVal方法,并且初始设定了hash(key), key, value, false, true五个值,其中hash()方法是对key求哈希码。

我们点开putVal方法

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node 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 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)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;
    }

这是本博客最重要的方法,我们先看第一行

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {

底层代码用final修饰,表示不可修改,定义了五个传入的参数,分别是int类型的hash,键key,值value,boolean类型的两个参数onlyIfAbsent和evict

 Node[] tab; Node p; int n, i;

定义节点Node,其中显然tab类似于数组,而p类似于数组里面的元素。

Node[] tab; Node p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

把table赋值给tab,在我们的代码里面并没有出现table,其实点进去就知道,table是HashMap类里面的全局变量,且没有被赋值!

因此,table的初始值为null!也就是说(tab = table) == null成立!因为是或运算,有任一成立即可,继续往下。

我们发现出现了resize方法,顾名思义似乎用来重置长度的,我们点进去——

    final Node[] resize() {
        Node[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        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;
            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[] newTab = (Node[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node 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)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        Node 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;
    }

这么长的代码不用担心,我们只需要抓住关键几行即可。

首先,定义了oldTab并将table赋值给它

Node[] oldTab = table;

然后在代码的中间出现了这么一行

table = newTab;

很显然,table又被赋值了newTab 的属性。

最后返回的是newTab

return newTab;

那么我们可以确定,newTab等于table,他们的地址指向一致。

现在回归put方法代码

Node[] tab; Node p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

newTab又被赋值给了tab,显然tab此时依然等于table。

        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

hash就是我们在putVal方法最开始输入的hash,他是通过如下方法得到的

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

key即是我们输入add方法中的值

如果key的值为null,返回零,否则返回key 的哈希码(这里规定了哈希码大于等于16)。

也就是说,每输入一个key值,我们给他动态生成一个唯一的哈希码并乘上n-1。

现在令i=这个值,然后以i为索引,让tab[i]给p赋值,并判定tab[i]是否等于null。

对与一个节点来说,这换代码是判断这个节点里面是否存贮有数据。

显然,这是我们第一次输入,节点里面肯定没有任何数据,

if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

继续执行,tab[i]被赋值,我们看到被一个一个新节点方法赋值,这其中newNode 的方法代码为

    Node newNode(int hash, K key, V value, Node next) {
        return new Node<>(hash, key, value, next);
    }

显然。null是对next的赋值,next,顾名思义是下一个,在这里,下一个节点的里面的数据被规定为null。

后面庞大的else代码块可以直接略过。

return null!

即putVal方法返回null!

即put方法返回null!

即map.put(e, PRESENT)==null成立为true!

即add方法返回true!

添加成功!

我们意识到,HashSet本质上还是HashMap,而HashMap本质上还是Node对象数组。Node是节点。

第二次添加同样的

package add;

import java.util.HashSet;

public class Test {

	public static void main(String[] args) {
		HashSet list=new HashSet<>();
		String name="Tom";
		System.out.println(list.add(name));
		System.out.println(list.add(name));
	}

}
 
  

但是第二次添加name输出结果却为false,我们知道这是由于传入的key值实际上是HashMap的key值,而key值不允许重复所以添加失败。现在我们来详细分析代码。

现在直接对最重要的putVal进行分析——

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node 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 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)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;
    }

第一行if语句显然成立,因为这是又一次调用方法,tab和table都刷新了。

但是对于第二个if代码而言。

        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

因为添加的对象一样,所生成的hash码也一样,即,tab[i]的位置和第一次一样,这个时候tab[i]的位置中以及有第一次赋的数据了,因此不为null!执行else语句。

Node e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

我们来看else的代码块中第一个嵌套的if语句。

首先定义了节点e和key值k

首先要判定p.hash=hash。

hash我们知道,是putVal方法里面输入的值,本质上是传入的key值生成的哈希码,p.hash是什么?我们点进去看看——

    static class Node implements Map.Entry {
        final int hash;
        final K key;
        V value;
        Node next;

出现一个节点的方法,我截取了一部分,可以看到,这个hash是该静态方法定义的final修饰的全局变量。

p是节点,调用hash,尽管第二个if判断语句不是true,但是在判断语句之中给p赋值的行为依然有效!

因此,tab[i]还是赋值给了p,这个时候,tab[i]中的hash ,key value也被赋值了,且等于第一次用add方法时,用户赋的值。

if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

那么这个判断就很好理解了,就是判断p中的属性,到底等不等于第一次用户赋的值,对于我们重复添加相同对象这一行为,显然是等于的。(其他原因因为个人所学水平原因,暂时不足以讨论,我们只讨论第二次相同赋值。)

转进到这个if判断语句

if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;

e=p,值为第一次赋的值,显然不为null。

在方法之中我们又定义了value类型的值oldValue并把value赋给它(其实就是add方法里面那个常量)

然后下面是if判断

if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;

如果因为传入的时候onlyIfAbsent就被规定为false,所以!onlyIfAbsent为true,下面的代码一定会执行!

返回oldValue,其实就是第一次被赋的值,也就是add方法里面那个常量!

即putVal方法返回oldValue

即put方法返回oldValue

即map.put(e, PRESENT)==null不成立为false!

即add方法返回false!

添加失败!

你可能感兴趣的:(add方法,底层代码)