Java集合-Map

Java集合-Map

一、简介

Map是以键值对来存储数据元素的。键值对之间存在映射关系,通过key可以查找value。需要注意的是key是不允许重复的,可以认为Map的key组成的集合是一个Set,上篇文章我们介绍Set时也发现Java中Set的实现大多数最后都是采用Map来存储数据。

二、Map子类

HashMap

底层使用哈希表实现,需要实现hashcode和equals方法。面试中涉及最多的还是和HashTable的区别。首先前者是线程不安全的但效率较高key或vale均可为空,后者是线程安全的但线程安全是通过synchronized关键字实现的,key/value不能为空。

需要掌握的内容:

  • 底层实现

java1.8版本为数组+链表+红黑树实现,1.7为数组+链表。

    //内部定义了一个table数组来存放键值对,
    transient Node<K,V>[] table;
    
    //内部类Node表示一个键值对
    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;
        }

从上面可知HashMap使用内部类Node表示一个键值对,且定义了一个数组来存储键值对,存储过程是根据hash值来确定存储位置的,当发生hash冲突时会把后到的元素链接到上一个Node,当同一index的链表长度超过一定值后会把链表调整为红黑树。

  • 存取元素过程

        public V put(K key, V value) {
          //通过hash(key)计算key的hash值
            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;
            //初始化table数组大小
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
                //根据hash值查找在数组中的index,如果找到的index位置上没有元素那么直接放置在该位置
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            else {
                //找到的index已存在元素即发生了hash碰撞
                Node<K,V> e; K k;
                 //如果当前index的元素与要存入元素hash一样且key相等则将p赋值给e
                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;
        }
    

    HashMap存储过程使用put,put流程如下:

    1. 先通过hash函数定位桶位置,如果桶位置上元素为空则直接插入该位置
    2. 如果桶位置上元素不为空,判断key是否存在,如果key为空则直接覆盖
    3. key如果不为空那么判断当前元素是否是TreeNode类型,如果是那么红黑树直接插入
    4. 如果不是TreeNode类型那么遍历链表,如果key在链表中存在那么直接覆盖value
    5. 如果key在链表中不存在那么插入链表,插入后判断链表长度如果大于8那么调整链表为红黑树。
    6. 最后插入完要判断当前size是否大于临界值,如果大于要进行扩容
  • 扩容过程

        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) {
                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;
            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
                            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对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组。

    1. resize首先会检查当前容量,如果等于0那么表示初始化
    2. 如果大于0那么会检查当前容量是否达到最大容量阈值,如果是那么新阈值设置为Integer.MAX_VALUE
    3. 如果未达到最大容量阈值,那么会扩容为当前容量的两倍。
    4. 扩容后需要对原数组中的元素进行重hash,Java 1.8版本重hash时不会重新计算hash只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”
  • hash的实现

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

    HashMap的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算

    1. 首先会获取key的hashCode值
    2. 然后hashCode值会跟hashCode值的高16为做异或运算
    3. 最后拿上两步获取的值对底层数组长度取模,取模操作使用的是h & (table.length -1)

    通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

参考链接:

https://blog.csdn.net/u013132758/article/details/89181005

Java8系列之重新认识HashMap

准备用HashMap存1w条数据,构造时传10000还会触发扩容吗

面试必问的HashMap,你真的了解吗?

TreeMap

TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。底层是二叉树(红黑树)实现。containsKey、get, put、 remove等操作的时间复杂度为log(n)

Hashtable

很多的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段机制(jdk1.7是分段锁 jdk1.8采用CAS+synchronized实现)。当不需要线程安全的场合可以用HashMap替换,需要线程安全且高并发的场合可以用ConcurrentHashMap替换

LinkedHashMap

LinkedHashMap是HashMap的一个子类,双向链表维护键值对次序,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

在内部定义了LinkedHashMapEntry,它继承自HashMap.Node,增加了before, after指针。

    static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
        LinkedHashMapEntry<K,V> before, after;
        LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

LinkedHashMap是 LruCache核心。

LinkedHashMap都有哪些小秘密?同时给LruCache 提个建议

LinkedHashMap源码解析(JDK8)

IdentityHashMap

HashMap内对key比较时是使用hash和equals,IdentityHashMap则是使用= =实现的

    public boolean containsKey(Object key) {
        Object k = maskNull(key);
        Object[] tab = table;
        int len = tab.length;
        int i = hash(k, len);
        while (true) {
            Object item = tab[i]
            //k和数组中某个已存在key通过“= =”比较,判断是否存在
            if (item == k)
                return true;
            if (item == null)
                return false;
            i = nextKeyIndex(i, len);
        }
    }

WeakHashMap

如果其中key所引用对象没有被其他对象以强引用所引用那么key就会被回收。如果key所对应的对象被回收那么其表示的键值对会被删除。

ConcurrentHashMap

因为hashtable同步效率较低所以引入ConcurrentHashMap用于处理并发情况的 HashMap。大概的实现机制是分段,每段有自己的锁,这样就有多把锁可以在一定程度提高同步效率。感兴趣的可以自行去了解具体实现。

https://www.jianshu.com/p/d0b37b927c48

ArrayMap

它的内部实现是基于两个数组。
一个int[]数组,用于保存每个item的hashCode.
一个Object[]数组,保存key/value键值对。容量是上一个数组的两倍。
它可以避免在将数据插入Map中时额外的空间消耗(对比HashMap)。
而且它扩容的更合适,扩容时只需要数组拷贝工作,不需要重建哈希表。
和HashMap相比,它不仅有扩容功能,在删除时,如果集合剩余元素少于一定阈值,还有收缩(shrunk)功能。减少空间占用。

深度解读ArrayMap优势与缺陷

SparseArray

SparseArray是Android提供的key为int类型的map,底层使用数组实现。其内部有两个数据int[] mKeys和Object[] mValues。

mKeys和mValues通过如下方式对应起来:

  • 假设要向SparseArray存入key为 10,value为200的键值对,则先将10存到mKeys中,假设 10 在mKeys中对应的索引值是index ,则将value存入 mValues[index]中
  • mKeys中的元素值按照递增的形式存放,每次存放新的键值对时都通过二分查找方法来对mKeys进行排序

SparseArray避免了基本数据类型的拆装箱操作,且在数据量不大的情况下查找效率高(内部使用二分查找),还有就是其移除数据时仅做数据标记并不实际删除,实际删除操作在垃圾回收时才进行。

参考链接

三、小结

至此Java集合相关类基本分析完毕
这里贴一个在网上看到的简单规律总结:
ArrayXxx:底层数据结构是数组,查询快,增删慢
LinkedXxx:底层数据结构是链表,查询慢,增删快
HashXxx:底层数据结构是哈希表。依赖两个方法:hashCode()和equals()
TreeXxx:底层数据结构是二叉树。两种方式排序:自然排序和比较器排序

参考:

面试中的HashMap、ConcurrentHashMap和Hashtable

posted @ 2020-09-20 19:05  Robin132929  阅读(172)  评论(0编辑  收藏  举报