HashSet中add方法源码详解

HashSet中add方法源码详解

HashSet简介

HashSet是Java容器框架中的一个重点,它是Set的实现类,继承了AbstractSet, HashSet是用来存放单值不重复的容器,其特点是对于存入的值,不保证其存入的顺序,以及不能重复,HashSet允许键值为null,但只能有一个null键值。下面我们从创建一个空的HashSet开始,然后向里面添加元素,一步步的分析源码来认识HashSet是怎样实现的。

源码分析

在进行源码分析之前,我们先写一段代码,然后根据这段代码一步步的深入源码,进行分析。代码很简单,如下:

    Set<String> set = new HashSet<>();
    set.add("110");
    set.add("110");
    set.add(new String("110"));
    System.out.println(set.size())

1.构造方法

先看代码第一行,Set<String> set = new HashSet();,从字面意义上来理解,有以下几点:

  1. 这句代码调用了HashSet无参的构造方法,创建了一个新的HashSet对象
  2. 将对象的引用赋值给了它实现的接口Set
  3. 对于Set中的元素传入泛型参数String,这样在编译期间,这个Set里面能够存放的数据类型就只能是String类型(String类是final的,不能被继承)。

HashSet无参构造方法

下面来看HashSet的构造方法源码:

    private transient HashMap<E,Object> map;
        // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
    public HashSet() {
            map = new HashMap<>();
        }

代码第一行是声明了一个HashMap类型的成员变量map,泛型内指定的类型为<E, Object>。这里可以先抛出结论,HashSet的底层实现是使用HashMap来实现的,HashSet利用了HashMap中key值不重复的特点,用来存储自己的元素,而map中对应的的value就是第二行的静态常量 PRESENT,是一个Object对象。稍后我们可以验证这一点。
根据以上代码,可以看到,HashSet的无参构造方法被调用时,只是调用HashMap的无参构造方法初始化了成员变量map,下面我们看HashMap的无参构造方法。

HashMap无参构造方法

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

可以看出,HashMap的无参构造方法也很简单,只是设置了一下成员变量加载因子loadFactor的值,值是一个静态常量0.75f,并未对其容量进行设置。然后我们看HashMap中用来存储数据的成员变量是怎么声明的:

/**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;

文档注释中已经说明,这个table在第一次使用的时候才会初始化,而且一定会会在第一次使用的时候调整大小。table的长度一定是2的幂。我们再看一下size属性的声明:

/**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

因此我们可以得出一个结论:使用无参方法构造创建HashSet对象以及HashMap对象时,它的初始容量是0(而不是很多人说的默认容量就是16),只有在第一次使用的时候对它进行初始化。

2.向HashSet中添加元素

下面看代码第二行:

set.add("110");

调用set对象的add方法,如果我们直接点add进入源码,会发现我们进入的是Set接口的源码,这是因为Java多态的特性,在编译期看到的是父类的方法,但是在运行期会识别运行的类型,调用方法时,如果发现子类重写了父类的方法,就会直接调用子类的方法。所以我们直接看HashSet的add方法源码就行,代码如下:

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

这里就已经验证了我们上面说的,利用map中key值不重复的特点存储元素,至于map中的value,存的就是一个静态常量,值是一个Object对象。
继续看map的put方法:

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

发现put方法又调用了putVal方法,在查看putVal方法之前,先来分析一波:

  1. HashSet的add方法要添加的元素e,传入了HashMap的put方法的key值,而value传入的是一个静态常量
  2. putVal()传入的第一个参数调用了hash()方法,传入的参数是key值(也就是最初HashSet中add的元素),看名字就知道是计算hash先看这个方法是怎么处理的。代码如下:
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

可以看到,这是一个静态方法,接收的参数类型是一个Object,传入的key值被向上转型为Object。
下面看hash的计算,如果传入的对象是一个null值,直接返回0;否则会先计算key对象的hashCode(),这里运用了Java多态,如果key值对象重写了HashCode方法,就会调用重写的HashCode()方法,否则就会调用Object中的HashCode()方法。这里会调用String的hashCode方法。之后对得到的hashCode先进行无符号右移运算,再与之前的hashCode进行异或运算,最后计算的值返回。
结论:一个对象的hashCode()方法计算的值相同,hash()方法计算时的值也一定相同。
现在让我们回到putVal方法。

HashMap中的putVal()

此处建议点开源码跟着看。
我们先看putVal方法的定义:

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

返回值类型是一个泛型V也就是value的类型,是final的不能被重写。下面就参数列表中的值与put方法中传入的参数,putVal(hash(key), key, value, false, true);进行一一对应,方便查看后续代码。

形参 实参
hash hash(key),HashSet中要添加的元素经过计算后的hash值
key key ,也就是最初要添加的元素e,字符串“110”
value value HashSet中传入的静态常量PRESENT
onlyIfAbsent false
evict true

继续往下走:

Node<K,V>[] tab; Node<K,V> p; int n, i;

声明局部变量,tab为Node<K,V>类型的数组,p是一个Node节点,是一个单链表。n是数组的长度,i是下标(后面会看到)
往下走之前先看一下HashMap中Node的结构,代码如下:

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

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

可以看出,是一个静态内部类,其结构是一个单链表,实现了Map接口中的嵌套接口Enty。看到这里,就已经明白为何大家都说HashMap为什么底层实现是数组+单链表 了。
继续往下走:

if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

table是成员变量(上面有提到),这是我们第一次向里面添加元素,所以此时的table是null。然后把table的值赋值给tab,并判断是否为null,结果为真,短路或直接执行if中的语句。调用resize()方法,这个代码是HashMap扩容的方法,实现很复杂,现只摘取本次运行会执行的代码展示如下,并用备注进行说明:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;    //尚未初始化,table和oldTable都为null
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
                省略。。。
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;    //静态常量,默认容量,16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//根据默认加载因子0.75计算出需要扩容时的容量大小
        }
        省略n行。。。
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创建Node类型的数组,大小为默认容量16
    table = newTab;//并赋值给成员变量table
     if (oldTab != null) {//oldTable为null,不执行
         省略。。。
     }
     return newTab;//返回newTab,注意:此时成员变量table和newTab指向的是同一个对象
 }

resize()方法总结:第一添加元素时,会执行此方法,创建一个Node<>[]数组,长度为16,赋值给成员变量table并返回这个数组的引用
现在继续看之前的代码:

if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

此时的tab已经指向一个新的,长度为16的Node[]对象(与成员变量table相同),n的值为16。
继续往下走:

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

上面代码的意思:

  1. 根据hash值计算出tab数组中的下标,并把值赋值给p
  2. 如果p的值为null,执行语句,因为我们是第一次添加,肯定为null。
  3. 执行语句tab[i] = newNode(hash, key, value, null);,存入一个Node节点,根据上面Node节点的构造方法,其对应的分别是:
Node属性 传入的值
hash key的hash
key key值,也就是传入的"110"
value 静态常量PRESENT
next 下个节点,null
  1. tab[i = (n - 1) & hash],,由于HashMap的长度永远为2的幂,所以这段话就是利用位运算求余,等价于hash%n,但是效率比直接求余高的多。求余的原因也利用除留余数法进行散列,这样得到的下标就是一个散列的。
    继续走:
else {
      不执行,稍后第二次添加的时候再说,,此时省略。。。
    }
    ++modCount;
    if (++size > threshold)    //检查扩容
        resize();
    afterNodeInsertion(evict);
    return null;    //添加成功,返回null

回到put方法,同样返回null,回到HashSet的add方法:

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

添加成功,返回true。
到此,我们第一次的add就分析完毕了,继续第二行添加一个重复的对象

添加重复元素

废话不多说,直接跳到putVal()方法:

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) //此时if语句不执行了,已经不为空,但是依然会赋值,tab为成员变量table,n为长度
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)//根据hash计算出下标,由于String的内容一样,所以hashCode一样,所以i的值也与第一次一样,p为对应的下标中的元素。但是不为空了,执行else代码块
            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;
        /*
        *hashMap去重的关键,分析在下面特别摘出
        */
        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;    //key值已存在,更新对应key值的value
            afterNodeAccess(e);
            return oldValue;    //返回更新之前的value值
        }

HashMap去重的关键代码分析:

else {
        Node<K,V> e; K k;//此处e值如果为Null,表示添加成功,否则就是key值已存在
        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;
            }
        }

这段代码执行的前提条件:向里面存入元素时,根据hash值算出的table[]的下标,其对应的值不为空,就会执行上面的else代码块。下面逐行进行分析:

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

此时p中存储的是table[i]的值,也就是单链表的第一个节点。会先判断存入对象的hash值与当前存储的元素的hash值,如果不相同,直接执行后面的代码。hash值相同后会执行((k = p.key) == key ) || key != null && key.equals(k))),这句话先将存入的key值赋值给局部变量k,然后与要添加的元素的key值进行==比较,相同则表示内存地址一样,key值重复了。否则会进行非空和调用key值的equals方法去比较,如果结果为真,则表示对象相同。把p值赋值给e(在下面的代码if (e != null) { // existing mapping for key会对e值进行判断,null表示key值不重复,已存入Map中,非空时e值表示的是p的值,也就是已存在的key值重复的节点Node)。

else if (p instanceof TreeNode)
        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

执行到这里的时候,证明table[i]的第一个节点中的key值不相同,判断是否已经转成红黑树,如果已成转成红黑树,则向下转型为TreeNode去进行判断添加。TreeNode继承自LinkedHashMap中的静态内部类Entry,LinkedHashMap.Entry又继承自HashMap中的静态内部类Node。
继续向下看:

else {
        for (int binCount = 0; ; ++binCount) {
            if ((e = p.next) == null) {//更新e值,为下一节点。如果遍历的e值为null时,就表示要添加元素的key值与此链表上的所有元素都不重复
                p.next = newNode(hash, key, value, null);//新增节点Node并将要添加的元素存入
                if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st    //判断单链表的长度,如果长度大于等于8时,转成红黑树存储
                    treeifyBin(tab, hash);
                break;
            }
            if (e.hash == hash &&    //如果e值也就是下一节点,中的元素key值重复,直接中断循环,此时e值依旧不为空,表示有存在的key值
                ((k = e.key) == key || (key != null && key.equals(k))))
                break;
            p = e;    //更新p值,使其指向next节点
        }

这段代码块执行的前提条件:table[i]中第一个节点key值重复,而且尚未转成红黑树。循环内容是使用循环进行开始遍历单链表,直到遍历的下一个节点为null或者找到重复的key值为止。下一节点为null时,就表示就表示要添加元素的key值与此链表上的所有元素都不重复,此时将会直接添加一个新的节点,并将数据存入。已存在key值时,直接中断循环进入下面的代码if (e != null) { // existing mapping for key会对e值进行判断,null表示key值不重复,已存入Map中,非空时e值表示的是p的值,也就是已存在的key值重复的节点Node。
执行完毕,如果有重复的key,会更新key值对应的value值,并返回旧的value值。HashSet中value是一个静态常量,所以都一样。否则返回null,表示数据存入到HashMap中了。

posted @ 2021-05-11 23:31  Venking-  阅读(360)  评论(0编辑  收藏  举报