常用的Map集合

HashMap的一些特点及其原理

键不可重复,值可重复,底层哈希表存储(计算key的hash码的方式与Hashtable不同),线程不安全,允许key和value的值为null(根据key计算hashcode时进行了判空处理,如果key为空,hashcode=0)。普通情况下HashMap的哈希表采用拉链法进行实现,优化哈希函数和减小哈希因子可以降低哈希冲突,而哈希表采用拉链法进行存储就可能会出现同一个哈希地址链表过长的情况,这种情况下查询效率会变得很低下,JDK1.8对这个情况作出了应对策略进行了优化来避免拉链过长(链表过长),就是当链表长度大于8或者哈希表数组存放元素超过64的时候会采用哈希红黑树的方式进行存储。但是由于树相比链表会占用更多的资源,因此HashMap的哈希表也没有完全采用红黑树实现,而是在节点数量达到一定程度后才使用红黑树存储。

静态常量

哈希表里面存放所有链表的表头的数组有称为“桶”

    //内部存放节点的数组默认容量为16,并且必须是2的幂次方,如果数组容量不是2的幂次方,会存在空间浪费,具体原因涉及到十进制与二进制的转化这里不做探究。
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //哈希表存储方式由拉链法转为红黑树方法的节点数临界值,当链表节点大于8个时采用红黑树
    static final int TREEIFY_THRESHOLD = 8;
     //红黑树存放方式还原成链表的临界值,链表中元素个数小于这个值,就会把红黑树还原为链表结构,这个值应该比上面那个小,至少为 6,避免频繁转换
    static final int UNTREEIFY_THRESHOLD = 6;
    //哈希表采用红黑树实现的最小容量,只有当节点数大于64时,才能链表才能转化为树形。如果节点数小于64,就不会将链表转化为红黑树,而是用resize()代替
    static final int MIN_TREEIFY_CAPACITY = 64;

    /*自定义的字段*/

    //节点数组(桶),用于存放每一条链表的表头,在必要的时候会调用resize()对其进行扩容等操作,该数组的容量必须为2的幂次方
    transient Node<K,V>[] table;

    //存放所有具体元素的Set集合
    transient Set<Map.Entry<K,V>> entrySet;
    //当前map存放的元素数量
    transient int size;
    //当前Map结构被修改的次数
    transient int modCount;
    //执行resize操作的临界值,当HashMap的size大于threshold时会执行resize操作。
    int threshold;
    //自定义的负载因子字段,默认的负载因子为:DEFAULT_LOAD_FACTOR=0.75f
    final float loadFactor;

TREEIFY_THRESHOLD和MIN_TREEIFY_CAPACITY这两个是HashMap底层的哈希表是否使用红黑树实现的关键,这两个条件都要满足才会使用红黑树。

负载因子loadFactor与扩容临界值threshold有关,负载因子决定了,容器装多少数据时可以扩容,负载因子越大则散列表的装填程度越高,也就是能容纳更多的元素,元素多了,链表大了,所以此时索引效率就会降低。反之,负载因子越小则链表中的数据量就越稀疏,此时会对空间造成烂费,但是此时索引效率高。

HashMap的构造函数

   //指定初始化容量和负载因子的构造函数 
   public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //如果初始容量大于最大容量,则使用最大容量作为初始容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //如果负载因子必须是大于0的浮点数,否则会抛出异常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        //根据设置的初始化容量,计算出resize的临界值,tableSizeFor(initialCapacity)返回最近的不小于输入参数的2的整数次幂。比如10,则返回16。
        this.threshold = tableSizeFor(initialCapacity);
    }
    //指定初始化容量,使用默认的负载因子
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    //无参的构造函数,使用默认的负载因子,初始化容量,由于没有在构造函数里指定,在指定put()方法的时候,会执行到resize()方法,此时由于oldCap=0,所以将使用默认的初始化容量,即:DEFAULT_INITIAL_CAPACITY
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    //通过Map m构造新的Map,使用默认的负载因子作为新的Map的负载因子
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

HashMap的put方法源码

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    //计算key的哈希值,从这里可以看出,调用HashMap的put方法时key可以为空值
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab;//通过tab=table的操作,tab指向桶
        Node<K,V> p; //节点
        int n, i;
        //如果桶为空,或者桶的长度为0,则调用resize方法重新初始化桶
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //如果通过hash找到的位置(tab[(n-1)&hash])为空,则代表此时节点数组当前位置还没有一个节点,此时可以直接通过key和value生成的节点放入到此位置。
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //如果节点p(即:上面p=tab[i=(n-1)&hash])的hash值与当前key算出的hash值相等,并且节点p的key与当前Key相同或者通过equals判断相等,则 令 e = p
            //由于p=tab[i=(n-1)&hash]得出的,因此p.hash一定等于当前的hash
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                //如果节点p是红黑树节点,则将key和value放入红黑树中
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //经过上面的重重判断,可以确定如果代码运行到这里,代表p.key与当前的key是不相等的,则需要从p开始通过链表查询的方法,依次看后面的节点有没有与当前key相同的节点
                for (int binCount = 0; ; ++binCount) {
                    //这里设置e=p.next,并判断是否为空,如果为空,则此时e为链表的末端
                    if ((e = p.next) == null) {
                        //将key和value生成的节点添加链表的末尾
                        p.next = newNode(hash, key, value, null);
                        //此时binCount就代表链表里节点的总数减一(循环是从第二个节点开始的)如果此时链表里节点的个数大于临界值,则需要将链表转化为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果节点e的key与需要添加的key是相同的,并且不为空,则跳出循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //e此时就是通过key生成hash值找到的节点,如果不为空就代表,此时map里面存在相同的key,具有对应的值了
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                //如果onlyIfAbsent为false或者key对应的value为空,则替换value为当前添加的value值
                    e.value = value;
                //此方法HashMap里没有具体的实现,在LinkedHashMap里才会具体实现,这里使用了模板方法模式
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //计数器加一
        ++modCount;
        //如果此时Map里面的节点个数大于临界值了,则需要调用resize扩容
        if (++size > threshold)
            resize();
        //和上面的afterNodeAccess方法作用一样
        afterNodeInsertion(evict);
        return null;
    }

put方法的主要过程:

1.根据key计算出一个hash值

2.根据该hash值,在桶中找到相应的位置,如果没有发生哈希冲突,则可以直接新生成一个节点放入到桶的该位置里

3.如果发生了哈希冲突,则判断此时桶的相应位置的节点的key是否与当前添加的key相同,如果相同则表示当前Map中已经存在了相应的key

5.如果不相同,再判断桶相应位置的节点是不是红黑树节点,如果是则添加到红黑树中

6.如果不是红黑树节点,则添加到链表中,如果链表中的节点个数达到临界值TREEIFY_THRESHOLD并且哈希表存放桶的数组长度也达到临界值MIN_TREEIFY_CAPACITY,则将链表转化为红黑树

7.经过前面的步骤发现新添加key如果Map中已经存在,则替换value的值

8.如果节点个数已经达到容量的临界值了,调用resize扩容

经常用到的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;
        //先判断此时数组的容量是否大于0
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                //如果数组的容量已经大于等于设置的最大容量了,则修改扩容的临界值为Integer.MAX_VALUE;
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //先设置newCap等于oldCap的两倍
                //判断等于oldCap的两倍的newCap是否依然小于最大容量,并且oldCap大于等于默认的初始容量,如果是true,则设置新的临界值newThr为原临界值oldThr的两倍
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            //如果数组的容量小于或等于0,并且原扩容临界值大于0,则设置新的数组容量newCap为容量临界值
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            //如果数组容量小于或等于0并且扩容临界值小于等于0,则设置数组容量为默认容量,扩容临界值为 负载因子*默认容量
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
   
        if (newThr == 0) {
            //临界值如果等于0,计算并设置新的扩容临界值,新的扩容临界值和新的容量不能大于默认的最大容量,如果有一个大于或等于数组的最大容量了,则设置临界值为Integer.MAX_VALUE
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;//设置临界值
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        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)//原位置节点是红黑树
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //原位置为链表形式,则重新计算hash
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //如果(e.hash&oldCap)等于0,则保留
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                //如果不等于0,则放入新的链表,在下面会将新的链表在新的数组重新找一个位置存放,新位置为此时的位置j+oldCap
                                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) {
                            //这里就是讲重新hash的链表放入到新的节点数组对应的位置
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

调用resize方法,会遍历容器中所有的元素,是非常耗时的,也会重新分配内存,这些操作都是非常消耗资源的,所以要尽量避免resize,因此,使用Map的时候设置一个合适的节点数组容量和合适的threshold临界值有时候是很重要的。

关于HashMap的遍历:

HashMap的遍历就是根据节点数组里存放的首节点,来遍历后面的所有节点,下面是关于HashMap通过entrySet方法遍历的核心代码:

    public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }

    final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }
    }

    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

    abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot

        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() {
            return next != null;
        }
        //下面的方法,就是遍历每个节点
        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }
    }

这里关于HashIterator里面的nextNode这个方法要重点说明,为什么从方法里面我们看出遍历节点的时候好像并没有将红黑树考虑进去,因为TreeNode里面并没有next的属性指向下一个节点,但是其实这里TreeNode也能够使用next属性。在生成红黑树TreeNode节点是利用了继承的特点,TreeNode继承了LinkedHashMap.Entry<K,V>,而LinkedHashMap.Entry<K,V>继承了HashMap里的Node,在TreeNode的构造方法里首先调用了父类LinkedHashMap.Entry<K,V>的构造函数,而在LinkedHashMap.Entry<K,V>的构造函数里也首先调用了父类Node的构造函数来初始化Node的变量,因此就算HashMap里面存在拉链过长转化哈希表为红黑树的方法存储情况时,每个TreeNode节点也会继承Node里面next的属性,红黑树依然可以按照添加顺序以链表的形式得到。 

下面是HashMap.TreeNode、LinkedHashMap.Entry<K,V>、HashMap.Node的构造函数源码:

    //HashMap里TreeNode的构造函数
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }
    //LinedHashMap里Entry的构造函数
    Entry(int hash, K key, V value, Node<K,V> next) {
       super(hash, key, value, next);
    }
    //HashMap里Node的构造函数
    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
   }

LinkedHashMap的一些特点及其原理

LinkedHashMap与HashMap在很多地方都是相同的,LinkedHashMap时HashMap的子类,键不可重复,值可重复,底层依然采用哈希表存储,线程不安全,允许key和value为空。只是在LikedHashMap重写了newNode()方法,在newNode方法中将每个新生成的节点与已经存在的节点用双向链表链接起来了,其实LinkedHashMap中put方法就是使用的父类HashMap的中的put方法,只是重新的newNode方法实现了节点的双向链表结构,因此可以理解为LinkedHashMap既采用了哈希表(拉链法和红黑树)存储,也采用了双向链表存储。通过get方法查询元素时,如果设置accessOrder变量为false与HashMap查询元素是一样的,只是在当accessOrder为true时,表示按顺序查询元素,此时会修改双向链表的结构,将get方法访问到的节点移动到链表末尾。此外LinkedHashMap的遍历方式与HashMap也是不一样的,LinkedHashMap的遍历方式是遍历双向链表。HashMap的遍历是根据节点数组(桶)来遍历拉链节点。

LinkedHashMap重写的newNode方法源码:

   //这是重写的HashMap里的newNode方法,因此在LinkedHashMap在调用put方法时,put方法内部会调用newNode方法,如果是LinedHashMap对象,则会调用这个方法,在这个方法里具体做到了将每个新生成的节点用双向链表链接起来
   Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }
   private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;//如果此时容器为空,则tail为空
        tail = p;//tail始终是最后一个元素,p元素在后面的操作会放到链表的结尾
        //在添加第一个元素的时候,last=null,因此这是head也设置为p
        if (last == null)
            head = p;
        else {
        //last如果不等于null,则代表容器中已经存在元素了,则将p放在链表的末端
            p.before = last;
            last.after = p;
        }
    }

通过newNode方法和linkNodeLast方法,可以看出,LinkedHashMap只是在原有HashMap存放元素的基础上,通过双向链表将每个元素链接起来了。

LinkedHashMap的get方法源码

   public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            //如果accessOrder为true,在每次通过get方法访问过节点e后,会修改LinkedHashMap维护的链表结构,将e移动到链表结尾,注意:这个操作并不会影响元素e在哈希表中的存储
            afterNodeAccess(e);
        return e.value;
    }
    //将节点e放到链表的末尾
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

上面的代码,一定要注意:修改LinkedHashMap维护的双向链表结构,并不会影响哈希表结构的存储。

关于LinkedHashMap的遍历:

LinkedHashMap通过entrySet方法遍历时,时直接利用双向链表的结构,直接遍历链表的方法实现元素的遍历;

下面是遍历的核心源码:

   abstract class LinkedHashIterator {
        LinkedHashMap.Entry<K,V> next;
        LinkedHashMap.Entry<K,V> current;
        int expectedModCount;

        LinkedHashIterator() {
            next = head;
            expectedModCount = modCount;
            current = null;
        }

        public final boolean hasNext() {
            return next != null;
        }
        //根据上一个节点遍历下一个节点
        final LinkedHashMap.Entry<K,V> nextNode() {
            LinkedHashMap.Entry<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            current = e;
            next = e.after;
            return e;
        }
   }

从nextNode方法可以看出,LinkedHashMap里面元素的遍历就是通过遍历链表的方法实现。

TreeMap的特点

键不可重复,值可重复,key不能为空,底层采用红黑树存储。当TreeMap内部没有设置默认的Comparator时,那么需要TreeMap调用put方法添加的元素,key所属的类必须是实现了Comparator接口,因为TreeMap是用红黑树存储,是有序的,所以在添加元素的时候必须与之前已经存在的元素进行比较,而当TreeMap内部的比较器为空的时候,就需要调用key自己的比较方法了。 

关于SynchronizedMap和ConcurrentHashMap性能比较

查看源码,JDK1.8的ConcurrentHashMap采用了transient volatile HashEntry<K,V>[] table保存数据。在需要加锁的地方是对每个table数组元素加锁,在put方法源码中

synchronized (f) {...}

代码段见源代码中synchronized(f),因此可以减少并发冲突的概率,提高并发性能。

然而SynchronizedMap是采用互斥锁的方式

public V get(Object key) {
    synchronized (mutex) {return m.get(key);}
}

public V put(K key, V value) {
    synchronized (mutex) {return m.put(key, value);}
}

所以ConcurrentHashMap的put并发性更好,因此相同工作下ConcurrentHashMap花费时间更少。

而ConcurrentHashMap的get方法采用了与HashMap一样的思路,并没有加锁,所以性能上优于SynchrinizedMap的get方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值