JAVA学习笔记——容器之Map

来源:互联网 发布:it产品经理培训 编辑:程序博客网 时间:2024/06/03 23:01
本文是实现Map的几个接口的类的总结,文中引用了潘威威的博客的大部分内容,在此表示感谢。

Map的整体结构

Map是一种把键对象和值对象映射的集合,它的每一个元素都包含一对键对象和值对象。 Map没有继承Collection接口。

  • AbstractMap:实现了Map接口的抽象类。Map的基本实现,其他Map的实现类可以通过继承AbstractMap来减少编码量。
  • SortedMap:继承Map。保证按照键的升序排列的映射,对entrySet、keySet和values方法返回的结果进行迭代时,顺序就会反映出来。
  • NavigableMap:继承SortedMap,含有返回特定条件最近匹配的导航方法。
  • HashMap:Map接口基于哈希表的实现,是使用频率最高的用于键值对处理的数据类型。它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,特点是访问速度快,遍历顺序不确定,线程不安全,最多允许一个key为null,允许多个value为null。可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap类。
  • HashTable:Hashtable和HashMap从存储结构和实现来讲有很多相似之处,不同的是它承自Dictionary类,而且是线程安全的,另外Hashtable不允许key和value为null。并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以使用HashMap,需要线程安全的场合可以使用ConcurrentHashMap。
  • LinkedHashMap: LinkedHashMap继承了HashMap,是Map接口的哈希表和链接列表实现。它维护着一个双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。
  • WeakedHashMap: 以弱键实现的基于哈希表的Map。在WeakHashMap中,当某个键不再正常使用时,将自动移除其条目。
  • TreeMap : Map接口基于红黑树的实现。
1. HashMap

  HashMap的数据结构是数组+链表+红黑树(红黑树since JDK1.8)。我们常把数组中的每一个节点称为一个桶。当向桶中添加一个键值对时,首先计算键值对中key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这种现象称为碰撞,这时按照尾插法(jdk1.7及以前为头插法)的方式添加key-value到同一hash值的元素的后面,链表就这样形成了。当链表长度超过8(TREEIFY_THRESHOLD)时,链表就转换为红黑树。

        

HashMap中有以下三个成员变量需要特别注意,

    transient Node<K,V>[] table; // 扩容时按两倍增加    transient int size; // 当前Map中数据数量    int threshold; // 当size大于threshold,Map就进行扩容。threshold初始时等于 initialCapacity * loadFactor,扩容时2倍增加    final float loadFactor;

HashMap中根据Key计算所在桶的位置的方法如下:

hash =    hash(key);first = tab[(n - 1) & hash]);static final int hash(Object key) {        int h;        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);    }

Resize:

  向hashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,hashMap就需要扩大数组的长度,以便能装入更多的元素。当然数组是无法自动扩容的,扩容方法使用一个新的数组代替已有的容量小的数组。resize方法非常巧妙,因为每次扩容都是翻倍,与原来计算(n-1)&hash的结果相比,节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置。
/** * 对table进行初始化或者扩容。 * 如果table为null,则对table进行初始化 * 如果对table扩容,因为每次扩容都是翻倍,与原来计算(n-1)&hash的结果相比,节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置。 */final Node<K,V>[] resize() {    //新建oldTab数组保存扩容前的数组table    Node<K,V>[] oldTab = table;    //使用变量oldCap扩容前table的容量    int oldCap = (oldTab == null) ? 0 : oldTab.length;    //保存扩容前的临界值    int oldThr = threshold;    int newCap, newThr = 0;    //如果扩容前的容量 > 0    if (oldCap > 0) {        //如果当前容量>=MAXIMUM_CAPACITY        if (oldCap >= MAXIMUM_CAPACITY) {            //扩容临界值提高到正无穷            threshold = Integer.MAX_VALUE;            //无法进行扩容,返回原来的数组            return oldTab;        }        //如果现在容量的两倍小于MAXIMUM_CAPACITY且现在的容量大于DEFAULT_INITIAL_CAPACITY        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)            //临界值变为原来的2倍            newThr = oldThr << 1;     }//如果旧容量 <= 0,而且旧临界值 > 0    else if (oldThr > 0)         //数组的新容量设置为老数组扩容的临界值        newCap = oldThr;    else {//如果旧容量 <= 0,且旧临界值 <= 0,新容量扩充为默认初始化容量,新临界值为DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY        newCap = DEFAULT_INITIAL_CAPACITY;        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);    }    if (newThr == 0) {//在当上面的条件判断中,只有oldThr > 0成立时,newThr == 0        //ft为临时临界值,下面会确定这个临界值是否合法,如果合法,那就是真正的临界值        float ft = (float)newCap * loadFactor;        //当新容量< MAXIMUM_CAPACITY且ft < (float)MAXIMUM_CAPACITY,新的临界值为ft,否则为Integer.MAX_VALUE        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?                  (int)ft : Integer.MAX_VALUE);    }    //将扩容后hashMap的临界值设置为newThr    threshold = newThr;    //创建新的table,初始化容量为newCap    @SuppressWarnings({"rawtypes","unchecked"})        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];    //修改hashMap的table为新建的newTab    table = newTab;    //如果旧table不为空,将旧table中的元素复制到新的table中    if (oldTab != null) {        //遍历旧哈希表的每个桶,将旧哈希表中的桶复制到新的哈希表中        for (int j = 0; j < oldCap; ++j) {            Node<K,V> e;            //如果旧桶不为null,使用e记录旧桶            if ((e = oldTab[j]) != null) {                //将旧桶置为null                oldTab[j] = null;                //如果旧桶中只有一个node                if (e.next == null)                    //将e也就是oldTab[j]放入newTab中e.hash & (newCap - 1)的位置                    newTab[e.hash & (newCap - 1)] = e;                //如果旧桶中的结构为红黑树                else if (e instanceof TreeNode)                    //将树中的node分离                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);                else { //如果旧桶中的结构为链表。这段没有仔细研究                    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;}
从代码中可以看到,扩容很耗性能。所以在使用HashMap的时候,先估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。看完代码后,可以将resize的步骤总结为:   
  a.计算扩容后的容量,临界值。
  b. 将hashMap的临界值修改为扩容后的临界值根据扩容后的容量新建数组,然后将hashMap的table的引用指向新数组。
  c. 将旧数组的元素复制到table中。

     putVal:
    向HashMap中插入数据调用的是put方法,put又调用的内部putVal,此处直接分析putVal的源码:
 /**     * Implements Map.put and related methods     *     * @param hash hash for key     * @param key the key     * @param value the value to put     * @param onlyIfAbsent if true, don't change existing value     * @param evict if false, the table is in creation mode.     * @return previous value, or null if none     */    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方法可以分为下面的几个步骤:
  a. 如果哈希表为空,调用resize()创建一个哈希表。
  b. 如果指定参数hash在表中没有对应的桶(tab[i = (n - 1) & hash]== null),即为没有碰撞,直接将键值对插入到哈希表      中即可。
  c. 如果有碰撞,遍历桶,找到key映射的节点 
    (1) 桶中的第一个节点就匹配了,将桶中的第一个节点记录起来。
    (2) 如果桶中的第一个节点没有匹配,且桶中结构为红黑树,则调用红黑树对应的方法插入键值对。
    (3) 如果不是红黑树,那么就肯定是链表。遍历链表,如果找到了key映射的节点,就记录这个节点,退出循环。如果没有        找到,在链表尾部插入节点。插入后,如果链的长度大于TREEIFY_THRESHOLD这个临界值,则使用treeifyBin方法把链        表转为红黑树(红黑树是采用hash值比较大小的,小左大右)。
 d. 如果找到了key映射的节点,且节点不为null
   (1) 记录节点的vlaue。
   (2) 如果参数onlyIfAbsent为false,或者oldValue为null,替换value,否则不替换。
   (3) 返回记录下来的节点的value。
 e. 如果没有找到key映射的节点(b、c步中讲了,这种情况会插入到hashMap中),插入节点后size会加1,这时要检查size是     否大于临界值threshold,如果大于会使用resize方法进行扩容。

 HashMap的其他方法也是按照上面结构图所展示的准则来处理的,就不再做具体的介绍了。
 HashMap不是线程安全的,如果想使用线程安全的HashMap,除了使用ConCurrentHashMap外,还可以使用Collections.synchronizedMap对HashMap进行封装,它使用synchronized关键字同步信号量,重写了HashMap的公共方法,因此性能不高。

2.LinkedHashMap
 LinkedHashMap继承自HashMap,主要多了两个成员变量head和tail,LinkedHashMap处理保持的上面HashMap的桶结构外,还用head和tail将插入数据按先后顺序用链表链接起来,因此LinkedHashMap重写了HashMap的迭代器方法。

3.TreeMap
 TreeMap是用红黑树存储数据的,它是有序的,它必须要有一个Key的比较器comparator,或者数据的Key必须是实现了Comparable接口的,否则插入数据会抛强转异常。

4.HashTable
 HashTable继承自Dictionary,它的实现和HashMap大同小异,通过在public的方法上加synchronized关键字实现线程安全,但是HashTable的性能并不高,不建议使用。

5.ConcurrentHashMap
 ConcurrentHashMap是线程安全的,它同HashMap的数据结构是一样的,不同之处在于它是通过分段锁来实现线程安全的,在插入或删除节点时,先同hash找到节点所在的桶,再用synchronized关键字锁定桶后再进行操作,最大的保证了性能同时保证线程安全。(更详细内容暂时没看懂,后面再补充)

final V replaceNode(Object key, V value, Object cv) {        int hash = spread(key.hashCode());        for (Node<K,V>[] tab = table;;) {            Node<K,V> f; int n, i, fh;            if (tab == null || (n = tab.length) == 0 ||                (f = tabAt(tab, i = (n - 1) & hash)) == null)                break;            else if ((fh = f.hash) == MOVED)                tab = helpTransfer(tab, f);            else {                V oldVal = null;                boolean validated = false;                synchronized (f) {                    if (tabAt(tab, i) == f) {                        if (fh >= 0) {                            validated = true;                            for (Node<K,V> e = f, pred = null;;) {                                K ek;                                if (e.hash == hash &&                                    ((ek = e.key) == key ||                                     (ek != null && key.equals(ek)))) {                                    V ev = e.val;                                    if (cv == null || cv == ev ||                                        (ev != null && cv.equals(ev))) {                                        oldVal = ev;                                        if (value != null)                                            e.val = value;                                        else if (pred != null)                                            pred.next = e.next;                                        else                                            setTabAt(tab, i, e.next);                                    }                                    break;                                }                                pred = e;                                if ((e = e.next) == null)                                    break;                            }                        }                        else if (f instanceof TreeBin) {                            validated = true;                            TreeBin<K,V> t = (TreeBin<K,V>)f;                            TreeNode<K,V> r, p;                            if ((r = t.root) != null &&                                (p = r.findTreeNode(hash, key, null)) != null) {                                V pv = p.val;                                if (cv == null || cv == pv ||                                    (pv != null && cv.equals(pv))) {                                    oldVal = pv;                                    if (value != null)                                        p.val = value;                                    else if (t.removeTreeNode(p))                                        setTabAt(tab, i, untreeify(t.first));                                }                            }                        }                    }                }                if (validated) {                    if (oldVal != null) {                        if (value == null)                            addCount(-1L, -1);                        return oldVal;                    }                    break;                }            }        }        return null;    }
6.WeakHashMap
  WeakHashMap继承自AbstractMap,它的键是弱键,即当一个键不再正常使用,键对应的键值对将自动从WeakHashMap中删除。它是采用数组存储数据,它计算数组索引的算法和HashMap略有区别,方法如下:
    final int hash(Object k) {        int h = k.hashCode();        // This function ensures that hashCodes that differ only by        // constant multiples at each bit position have a bounded        // number of collisions (approximately 8 at default load factor).        h ^= (h >>> 20) ^ (h >>> 12);        return h ^ (h >>> 7) ^ (h >>> 4);    }     private static int indexFor(int h, int length) {        return h & (length-1);    }
 此外,WeakHashMap没有实现用红黑树存储数据的逻辑。

    WeakHashMap有一个ReferenceQueue类型的成员变量queue,往WeakHashMap里插入数据时,会将生成Key的WeakReference,当key不再正常使用后,key的WeakReference就被JVM的垃圾回收器插入到queue中,在适当的时候(put、remove、size、resize基本上所有public的操作),WeakHashMap会根据queue里数据,移除对应的Value。移除方法如下所示:
 /**     * Expunges stale entries from the table.     */    private void expungeStaleEntries() {        for (Object x; (x = queue.poll()) != null; ) {            synchronized (queue) {                @SuppressWarnings("unchecked")                    Entry<K,V> e = (Entry<K,V>) x;                int i = indexFor(e.hash, table.length);                Entry<K,V> prev = table[i];                Entry<K,V> p = prev;                while (p != null) {                    Entry<K,V> next = p.next;                    if (p == e) {                        if (prev == e)                            table[i] = next;                        else                            prev.next = next;                        // Must not null out e.next;                        // stale entries may be in use by a HashIterator                        e.value = null; // Help GC                        size--;                        break;                    }                    prev = p;                    p = next;                }            }        }




参考文章:
 Java8源码-Map整体架构
 Java8源码-HashMap



原创粉丝点击