我是菜鸟:深入java容器内部

来源:互联网 发布:敬游软件 编辑:程序博客网 时间:2024/06/02 07:21

在此之前,容器部分的内容看过了很多次,但是对于其内部实现机制了解的比较少,准备在此系统的将之前的笔记整理下,以及将内部机制中一些比较好的idea记录下来。

HashMap

将HashMap作为第一部分介绍,并非其很简单,而是很多内容都是基于HashMap实现的,如HashSet。

HashMap概述

以下很多内容直接copy源码或者jdk API 中的内容。
1. The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls. 此处也列举除了hashtable 与HashMap 的含义。
2. Iteration over collection views requires time proportional to the “capacity” of the HashMap instance (the number of buckets) plus its size (the number of key-value mappings).
3. An instance of HashMap has two parameters that affect its performance: initial capacity and load factor.(初始容量与加载因子, 加载因子的大小影响了什么?空间消耗或者查询速度,然后区其折中为0.75f.) The default load factor (.75) offers a good tradeoff between time and space costs.
4. 如果提前知道加入元素的数量级,可以设置一个初始值,防止后面当元素的个数大于门限的时候,重新扩容,然后rehash, 这样比较浪费时间。

HashMap 代码分析

  1. This map usually acts as a binned (bucketed) hash table, but when bins get too large, they are transformed into bins of TreeNodes。 可以看到,当元素的个数比较多的时候,就不再使用hash table,而是转为树节点,具体的为红黑树节点。那么我们知道,红黑树是个特殊的排序二叉树,那么是不是必须要求实现Comparator 接口,这样红黑树才能排序呢?当然不是这样的,下面是解释:
    Tree bins (i.e., bins whose elements are all TreeNodes) are ordered primarily by hashCode, but in the case of ties, if two elements are of the same “class C implements Comparable C”, type then their compareTo method is used for ordering. 也就是说如果没有提供compareTo 这个方法的实现,那么就使用 hashCode来进行排序。(如何验证有没有提供compareTo接口呢?这个时候反射就有了用武之地。后面会详细的列出。)
    HashMap 什么时候效率比较低?当然是出现冲突的概率比较大的时候,即很大一本分元素的hashCode相同。
    Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). 因此当没有达到这个门限的时候,就转化为普通的HashMap (桶+链表的结构)。
  2. The default initial capacity - MUST be a power of two. 默认为16. 为了使得初始值为2的整数次方,我们看看jdk 是如何搞的。
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;    }

好复杂,有没有。也不懂为什么这样搞,还是觉得以前中的求解

while(capacity<initCapacity){capacity<<1;}

这样容易理解。
下面我们看下jdk中定义一些阀值的形式:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;

好多,有木有?真的是复杂,一步一步看吧。只是解释后面几个;
TREEIFY_THRESHOLD: 每个桶中元素个数达到使用数节点的阀值
MIN_TREEIFY_CAPACITY:整个HashMap中对应的数组的长度>=MIN_TREEIFY_CAPACITY时,而且某个桶中元素个数大于 TREEIFY_THRESHOLD 的时候,才进行树节点的替换。否则,如果小于MIN_TREEIFY_CAPACITY,而其中一个桶的元素个数大于TREEIFY_THRESHOLD,这时不会将该桶的元素转为树形结构,所做的操作为扩容。
3. HashCode的计算

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

我们可以看到得到hashCode后,用高16位与原来的值进行异或计算得到。
Because the table uses power-of-two masking, sets of hashes that vary only in bits above the current mask will always collide.(不知道怎么翻译。)
下面看下put方法

// 前一个方法省略了final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                   boolean evict) {                    Node<K,V>[] tab; Node<K,V> p; int n, i;// 判断当前的hashtable是否为空,这个if执行后,tab = table, n = table.length.        if ((tab = table) == null || (n = tab.length) == 0)            n = (tab = resize()).length;  // 新建hashTable.  // 注意计算 当前hash值对应的位置为  (n-1)&hash , 也就是 hash mod n 的结果。 如果当前桶中为空,那么直接添加。        if ((p = tab[i = (n - 1) & hash]) == null)            tab[i] = newNode(hash, key, value, null);        else {        // 当前的桶中先前已近有元素。            Node<K,V> e; K k;            //判断当前桶中的hash值是否与hash相等,若相等将p指向第一个,做后面的处理            if (p.hash == hash &&                ((k = p.key) == key || (key != null && key.equals(k)))) // 不知道为什么要判断这么多次?感觉直接第一个就ok了?                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);                        // 如果此时链表中节点个数大于TREEIFY_THRESHOLD这个阀值,那么转为树节点                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                            treeifyBin(tab, hash);                        break;                    }                    // 在查找的过程中,如果找到相同的hash值,直接跳出循环                    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;        // 如果大于threshold, 扩容        if (++size > threshold)            resize();        afterNodeInsertion(evict);        return null;    }

其思路就是:计算出key的hash值后,通过 hash & (table.length-1) 求出其对应的索引位置,如果当前索引位置为空,那么直接添加该元素后,结束,否则查看当前key是否已经存在,若存在,就更新现在的值;若不存在,那么首先判断是不是树节点类型,若是树节点类型,那么按照树的处理形式。(后面会看到)若是正常的链表,那么就遍历这个链表,在遍历到链表末尾之前,如果找到了当前的相同 的key,那么更新现在的value值就行, 若没有待插入的key,那么将其添加到链表的末尾,在添加之后,判断是否达到树节点的门限,若达到了树节点结构的门限,则转化为树结构, 最后再判断添加了新的节点后,整个hash表中的元素是否达到了扩容的标准,若达到了那么进行扩容。
下面我们看下resize代码:
先思考下,这个过程:首先按照扩容标准建立一个新的数组,然后针对原来的每个实例,重新计算hashCode,然后再逐个添加到新的数组中。
在对桶中的hash链表进行扩容的时候,原来的hash链表在新的hash表中分布在2个位置,即原来的位置处和(原来的位置+原来容量)对应的位置处。

final Node<K,V>[] resize() {        Node<K,V>[] oldTab = table;        int oldCap = (oldTab == null) ? 0 : oldTab.length;     // 获取原来HashMap数组的长度。        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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];        table = newTab;         // 按照桶的顺序,然后对桶中的每个元素进行重新hash        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)  // 测试是否为树节点,若为树节点进行处理, split() 方法。后面看看改方法。                        ((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;                        // 下面的一段代码,我靠,确实经典,也耗费我花了点时间,10来分钟才明白啥意思。                        //在此处,由于扩容之后,原来的一个桶中的元素,新hash出来的值只能为2种情况:hash值为0-oldCap-1, 以及oldCap-newCap 这2中情况。话说着不是废话吗?                        //这就是利用这个能够很快速的处理这些元素,而不用利用 hash & (newCap -1) 这样去计算。我们将上面2中情况分为2个链表,即低链表和高链表吧,                         //当元素的hash值与原来的hash值相与的结果为0的时候,说明其处于低链表中, 你说为什么?你举个例子就明白了,其数学原理,没有推导出来。                        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);                        // 将2个链表分别放入对应的地方。                        if (loTail != null) {                            loTail.next = null;                            newTab[j] = loHead;                        }                        if (hiTail != null) {                            hiTail.next = null;                            newTab[j + oldCap] = hiHead;                        }                    }                }            }        }        return newTab;    }

总结:说实话,感觉写的很复杂,但是效率确实比较高,代码也比较简洁,非我等小渣渣能写出来的。
下面我们看下treeifyBin这个函数,也就是如果将链表转为树结构时候如何操作的。

 final void treeifyBin(Node<K,V>[] tab, int hash) {        int n, index; Node<K,V> e;        //MIN_TREEIFY_CAPACITY参数终于用上了        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)            resize();            // 符合扩容的条件 即 2个门限的值都达到。(table长度>MIN_TREEIFY_CAPACITY且桶中元素个数>TREEIFY_THRESHOLD )        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);        }    }

感觉整个put的过程基本上分析完了,当然对于树形节点来说其如何建立树结构,添加,将在后面分析。总结一下,put操作的过程:
1. 计算出hash并定位到相应的桶;
2. 若桶为空,直接加入元素,否则执行下面第6 步
3. 判断定位的桶是否为树形节点,若是树形节点,按照插入树节点进行操作,否则按照链表进行处理:如果有key已在链表中,直接更新value,结束,否则,插入链表的末尾;
4. 判断是否达到将该桶转为树形结构的条件,若满足执行步骤4, 否则执行步骤5
5. 若满足则将该桶链表转为树形结构;
6. 判断整个HashMap的元素个数是否到达阀值,如没有,则结束,否则执行下面步骤6;
7. 扩容,重新定义hashMap.
上述过程可能不太完备,大概的思想就是如此。我觉得大师的代码风格,确实值得我们学习。下面是remove代码的分析。
下面是流程图(可能有错,请指出,谢谢):

这里写图片描述

 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) {            Node<K,V> node = null, e; K k; V v;            if (p.hash == hash &&                ((k = p.key) == key || (key != null && key.equals(k))))                node = p;   // 桶中的第一个元素即为需要要删除的点,否则按照树形查找到该key对应的节点或者链表对应的节点,node指向该节点            else if ((e = p.next) != null) {                if (p instanceof TreeNode)                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);                else {                    do {                        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)                    tab[index] = node.next;                else                    p.next = node.next;                ++modCount;                --size;                // 该函数  注释为 Callbacks to allow LinkedHashMap post-actions,不明白                afterNodeRemoval(node);                return node;            }        }        return null;    }
0 0
原创粉丝点击