本文是实现Map的几个接口的类的总结

Map的整体结构



java mapper接口继承 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)时,链表就转换为红黑树。

        

java mapper接口继承 map继承什么接口_java_02

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;
                }
            }
        }