Set、Map源码分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_24477797/article/details/81019948

本篇分析Set、Map。

先看下Map

常见的Map,有HashMap、Hashtable、ConcurrentHashMap、TreeMap。本篇重点了解HashMap,其它的Map,重点比较下其与HashMap的异同。

HashMap:java.util.HashMap

构造器:

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

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

    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

最常用的就是默认构造方法。默认构造方法中定义了一个loadFactor,其作用我们可以从resize()方法中的三行代码看出:用于定义HashMap极值。

   float ft = (float)newCap * loadFactor;
   newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                (int)ft : Integer.MAX_VALUE);

极值主要用于HashMap的扩容,后续再提。

构造器中另一个参数是initialCapacity,作用是定义初始HashMap的长度。但是需要注意的是,HashMap内部有将该值进行转换为2的幂次方,详情请看tableSizeFor():

  /**
     * Returns a power of two size for the given target capacity.
     */ 
 static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

节点:

    transient Node<K,V>[] table;
    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;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

可以看出HashMap中,数据元素使用了Node数组来保存,需要注意的是,并非HashMap的所有元素都用这个数组来保存,这里仅仅保存用于定位用的Node集合,也就是桶排序里面所有首节点的数组集合,详情可以看后面“如何通过Key定位Node”这部分内容。Node继承于Map.Entry<K,V>,如此,我们想得到Key的set集合,可以通过Map的keyset方法获取。其次,在HashMap中,我们查找下一个节点,可以通过一个节点的next节点,依次查找下去,具体怎么定位,后面会解释。

Put方法

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

 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) // 1 
            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)))) // 2
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else { // 3
                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) // 4
            resize();
        afterNodeInsertion(evict);
        return null;
    }

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

put中我们重点需要了解的是两点,一点是关于Key的如何处理的,第二点是如何动态扩容的。

第一点:我们都知道HashMap的Key是可以为null,同时具备Set的特性,唯一性。

key可以为null:我们可以从hash()中可以看到,仅将key==null时,hash值置为0,但后续put操作依然并没有任何阻拦的地方(注意可能是版本问题,之前的HashMap针对key==null并非如此处理)。

唯一性:从putVal()的注释1处,可以看出,找到Key的下标,我们是通过(n - 1) & hash计算出来的。如果有看过排序算法中桶排序的同学,就可以知道,这里的下标其实就是桶的概念。至于桶中怎么放元素,可以从注释3看到,从这个下标节点第一个元素开始进行next遍历,如果next发现有空位,我们就将该值插入,若有tab上的key hash值和待插入的key一模一样,则是真正找到了key,直接修改该key对应的value。

第二点:从putVal()的注释4处可以看出,每次put,size有自加,再与极值进行比较,判断是否进行扩容。具体扩容,我们可以查看resize()方法:

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) { // 1
            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) // 2
            newCap = oldThr;
        else {               // 3
            newCap = DEFAULT_INITIAL_CAPACITY; //   DEFAULT_INITIAL_CAPACITY = 1 << 4; 
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) { // 4
            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) { // 5
            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
                       ...
                    }
                }
            }
        }
        return newTab;
    }

如果我们直接 new HashMap(),同时第一次插入的话,代码回执行注释3处,初始创建一个长度16的Node[],极值为12。集合我们上面说的扩容情况,即当我们数组内部使用量达到极值12时,会再次进行扩容。

如果构造器带参传入,则会执行注释2\4处代码。

如果是第二次进行扩容,则会执行注释1,可以看出新的数组长度newCap = oldCap << 1,为原先的2倍,极值newThr = oldThr << 1;也是原先的2倍。

Remove方法:

    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) { // 1
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) // 2
                node = p;
            else if ((e = p.next) != null) { // 3
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do { // 4
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p) // 5
                    tab[index] = node.next;
                else // 6
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

Remove方法,我们需要注意的一个是如何定位节点,另一个是如果实现移除的。

关于如何定位:我们从注释1处可以发现,我们先通过hash值初步定位数组下标,即找到桶;通过注释2,判断hash值、引用、equal来确定是否直接就是该节点;如果不是则进入注释3/4进行next依次遍历查找。

移除:从注释5可以看到,如果通过下标找到的刚好就是该节点,则将该下标位置,指向该节点的next位置;从注释6可以看出,如果不是刚好下标位置,是直接将该节点的上一个元素的next,指向该节点的next节点。

其它的Map方法因为篇幅有限就不展开分析了。

关于HashMap和Hashtable的区别,可以看这里http://www.importnew.com/24822.html,这篇文章讲的很好,不过他的HashMap源码,和我的有点小出路,应该是版本问题,可以先忽略。

ConcurrentHashMap,是目前代替Hashtable的方案,源码和HashMap长的类似,其不同点是,在操作数据时,会针对节点进行加锁(Hashtable是使用方法锁),确保同步。

再看下TreeMap

TreeMap是有序的,有序的体现在Map.Entry

static final class TreeMapEntry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        TreeMapEntry<K,V> left;
        TreeMapEntry<K,V> right;
        TreeMapEntry<K,V> parent;
        boolean color = BLACK;

        /**
         * Make a new cell with given key, value, and parent, and with
         * {@code null} child links, and BLACK color.
         */
        TreeMapEntry(K key, V value, TreeMapEntry<K,V> parent) {
            this.key = key;
            this.value = value;
            this.parent = parent;
        }
	...
}

可以看出,TreeMapEntry有左孩子、右孩子,双亲节点。就是典型的二叉树,注意还有个color字段,所以还是红黑数。

其次,TreeMap的Key不能为null。可以看下put()源码,有几次针对Key进行判空抛出空指针异常的地方。

  public V put(K key, V value) {
        TreeMapEntry<K,V> t = root;
        if (t == null) { // 1          
            if (comparator != null) {
                if (key == null) {
                    comparator.compare(key, key);
                }
            } else {
                if (key == null) {
                    throw new NullPointerException("key == null");
                } else if (!(key instanceof Comparable)) {
                    throw new ClassCastException(
                            "Cannot cast" + key.getClass().getName() + " to Comparable.");
                }
            }           
            root = new TreeMapEntry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        TreeMapEntry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) { // 2
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else { // 3
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do { // 4
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
// 5
        TreeMapEntry<K,V> e = new TreeMapEntry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

针对于TreeMap的put源码,我们分析下其实现过程。首先依然是查看如果定位Key的,

从注释1看,是检测根结点是否为空,为空则创建二叉树的根结点;从注释2/4看,查找Key使用了遍历二叉树中的先序遍历,如果找到(即最后一个alse,其实是if(cmp == 0)),则setValue()。注释5则是通过找到其双亲节点,再插入到双亲节点的孩子节点。

Remove方法依然是通过Key,使用先序遍历二叉树的方法找到指定的节点(TreeMapEntry):

 final TreeMapEntry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        TreeMapEntry<K,V> p = root;
        while (p != null) {
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
        return null;
    }

最后进行删除,具体删除过程就不进行了,其实也就是红黑数删除节点的概念,有兴趣的同学可以看下deleteEntry(TreeMapEntry<K,V> p)方法,篇幅有限,这里就不展开了。

Set集合

set集合,典型的有HashSet,TreeSet。

但是看完HashSet、TreeSet,大吃一惊,HashSet和TreeSet的代码简直少的可怜。HashSet居然内部持有HashMap,然后让Value使用同一个Object:

private static final Object PRESENT = new Object();

TreeSet同理。

通过上面我们对HashMap和TreeMap源码的研究,也发现,它们内部已经对Key按照Set集合的方式实现了,而通过keyset()获取到的也是set集合。

猜你喜欢

转载自blog.csdn.net/qq_24477797/article/details/81019948