java提供的map

hashMap

先说明hashmap是线程不安全的
参考此链接:http://www.importnew.com/20386.html
hashMap内部的实现比list复杂好多,内部是有数组加链表的形式存储的,而put的键值的hashCode值的低位计算值为其存储在数组的下标,而数组里指向的是一个链表,链表中存的是真正的数据,这里的hashCode低位计算值可能会相等,相等时就会遍历数组指向的这个链表的各个键值,判断键值是不是一样,一样就覆盖,不一样就要添加到此链表中,每次扩为原大小的2倍,这里为什么两倍这么多呢,因为在hashmap中使用了很多的位运算来提高效率,而二进制中升/进一位相当于十进制的乘2.

//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;
        if ((tab = table) == null || (n = tab.length) == 0)//判断若数组为空或长度为0时会进行扩容,这里的扩容是初始化长度,在初始化HashMap时不会初始化数组
            n = (tab = resize()).length;//保存扩容后的长度
        if ((p = tab[i = (n - 1) & hash]) == null)//如果插入数据的hashCode下标对应的数据为空,会对其初始化新的Node,Node是链表中的节点
            tab[i] = newNode(hash, key, value, null);//在初始化Node时,将其要插入的值存入其中,然后数组对应位置执行这个链表节点
        else {//来到这里的话就说明要插入的数组的对应下标上已经有节点/链表存在了
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))//判断指向的节点的键值与要插入的键值是否一样,一样就覆盖其值,这里是先放到了e中
                e = p;
            else if (p instanceof TreeNode)//这是jdk8引入的红黑树,这里的操作的目的是放入到红黑树中,而这里是获取树中存在的键值对象,如果树中没有这个对象则会创建并返回null,有的话就返回这个对象用于后面覆盖
                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);//创建一个节点,内容为要插入的数据,并将最后一个节点的next指向新的节点
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);//这里是jdk8的优化,在链表长度大于8是会将链表转换成红黑树
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))//存在一样的键值,退出遍历,此时的e就是这个存在的键值的引用
                        break;
                    p = e;//用p继续遍历
                }
            }
            if (e != null) { // existing mapping for key//这意思是链表中有一样的键值,需要进行覆盖
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//覆盖原值
                afterNodeAccess(e);//hashMap中这个是空实现
                return oldValue;//返回被覆盖的值,这里因为是覆盖所以直接返回不用后面的长度判断及增加
            }
        }
        ++modCount;
        if (++size > threshold)//长度+1,如果加了之后大于当前数组的长度,就进行扩容
            resize();//扩容
        afterNodeInsertion(evict);//hashMap中这个是空实现
        return null;
    }

计算存储的下标即扩容

因为默认的长度为16,在2进制中为4位数,所以在一开始会对键值的hashCode进行位运算获取低4位作为数组的下标,在数组为空或者长度为0(前两个会进行初始化)或者在插入后的长度大于数组的长度会进行扩容,扩容会对hashCode的低4位扩成低5位,而在十进制中就是2倍的意思,在扩大后会将原数组添加到新数组中,在添加的过程中会更新下标,因为原下标是4位计算的,要将其改为5位的计算.
注:在jdk7即之前会重新计算hashCode,但在jdk8进行了优化,原来的元素不重新计算hashcode,而是判断其高一位的二进制是1还是0,是1的话就加上该位的值来得到新的下标

//扩容的具体实现
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//获取原长度,若为空则默认为0
        int oldThr = threshold;//hashCode计算阈值,相当于计算的hashCode上限
        int newCap, newThr = 0;
        if (oldCap > 0) {//原数组长度大于0,也就是初始化过了
            if (oldCap >= MAXIMUM_CAPACITY) {//长度已经是最大值了,不进行扩容
                threshold = Integer.MAX_VALUE;//hashCode计算位数为全部,对象的hashCode是通过地址算出来的,正常不会出现相等
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)//新的长度小于最大值,原长度大于默认值
                newThr = oldThr << 1; // double threshold//提高hashCode计算位数
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;//数组长度小于0,计算阈值大于0的情况,将长度扩到计算阈值大小
        else {               // zero initial threshold signifies using defaults//初始化
            newCap = DEFAULT_INITIAL_CAPACITY;//默认大小16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//默认计算阈值为长度的0.75倍
        }
        if (newThr == 0) {//原数组长度大于0且计算阈值等于0或原数组长度等于0且阈值大于0的情况下
            float ft = (float)newCap * loadFactor;//计算新的阈值,loadFactor默认为0.75,在初始化时可以设置
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);//新阈值和新数组长度都没有超过上限就是用ft,否者就用int最大值
        }
        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)//这里是这个下标上只有一个节点,就把这个节点直接放到新的下标上,就像原来低4位计算没有重复的key那现在扩容到低5为计算也不会有重复的key值
                        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;
//如上边的注所说,jdk8在这里进行了优化,因为每次扩容2倍即2进制进一位,而链表中的键值的hashCode的这一位,如4位扩到5位,就是5位为0的会在一个链表上,为1的会在另一个链表上,这样就生成了两个链表,这两个链表中的各节点的键值的hashcode低5为时相等的,然后将这两个链表放到新数组对应的地方,就完成的链表的新键值的计算与迁移
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {//扩容的那位数为0
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {//扩容那位数不为0
                                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在获取元素的时候以获取的键值的hashCode作为数组下标(这里也会做低位运算处理),找到对应的节点,再看看第一个节点键值是不是要找的,不是就开始遍历这个链表,找不到返回null

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {//存储的数组不为空,且长度大于0,且数组对应位置上的节点不为空
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))//如果第一个节点就是,直接返回第一个节点的值
                return first;
            if ((e = first.next) != null) {//有下一个节点
                if (first instanceof TreeNode)//如果是红黑树,对红黑树进行遍历获取
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {//对链表进行遍历
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))//是这个键值就返回
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

IdentityHashMap

与HashMap一样,只是其在相等的判断使用了==,即需要对象地址相等才会相等

LinkedHashMap

参考链接:https://blog.csdn.net/justloveyou_/article/details/71713781
https://blog.csdn.net/ns_code/article/details/37867985
以HashMap为基础,将其中的所有节点用双向链表的形式连在一起

ConcurrentHashMap

参考链接:https://www.cnblogs.com/leesf456/p/5453341.html
线程安全的HashMap,在线程安全的情况下保存的较好的性能.
先看其构造函数,

ConcurrentHashMap()
ConcurrentHashMap(int initialCapacity) 
ConcurrentHashMap(Map<? extends K, ? extends V> m) 
ConcurrentHashMap(int initialCapacity, float loadFactor)
 ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)

构造函数可不传参也可传入参数"initialCapacity"来设置其初始大小,默认为16的长度,传入参数"loadFactor"设置其负载因数,此值为定义map的满的程度,默认为0.75

其获取元素的方法与hashMap类似,且没有加锁,那说好的线程安全在哪里呢
再来看看插入元素
插入元素的具体实现,部分实现与Hashmap很像

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {//死循环
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)//数组为空
                tab = initTable();//初始化数组
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//获取tab数组下标为 (n - 1) & hash的节点,为空时
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))//tab的i下标是否为null,为null就用new Node<K,V>(hash, key, value, null)替换
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

SparseArray

此集合使用了两个数组来存储key和value,其中key限制为int类型.初始默认长度10,每次扩容2倍,使用System.arraycopy()方法复制到新数组上
在插入元素的时候,会使用二分法在key数组中搜索key,若搜索到了会将其值覆盖,若搜索不到会在二分法最后判断的位置(其实也就是key数组进行排序后,新key的位置)插入key值,并在value数组对应的下标位置上插入新value值.
在取值时,会通过key值进行二分法搜索,如搜索成功会返回value数组中对应下标的value.
SparseArray有两个优势:(可能不止,大概说一下)
1.因为使用数组存储,占用的空间会比较少;
2.因为使用二分法存取值,所以存取值的效率会比较高;(这个是相对来说的,毕竟二分法也有最好最坏的情况)

ArrayMap

与SparseArray存储的形式类似,都为两个数组,但其不限制key值的类型,一个数组用于存储key的hashCode(hashcode数组),另一个数组用于存储key和value值(元素数组),存储的格式为一个键一个值,初始默认长度10,每次扩容2倍,使用System.arraycopy()方法复制到新数组上.
在插入元素时会获取键值的hashCode,并使用二分法查找hashcode数组,如查找到有对应的hashcode会去覆盖元素数组的键和值,若找不到则在二分法最后搜索的位置进行插入操作,并在元素数组中插入对应的元素.
在取值的时候会获取使用二分法在hashcode数组中查找对应的位置,有则返回元素数组对应的值.
ArrayMap的优势与SparseArray类似,但其多了一个hashcode数组的开销,所以内存理论上是SparseArray的1.5倍,但正常情况下会不止1.5倍

WeakHashMap

内部逻辑与HashMap基本一样,相对于HashMap,其对键值的引用时弱引用,且在键值被GC时,将其加入ReferenceQueue(此队列记录了被GC的对象),在下次操作(size.get.put等操作)时会进行清除被回收对象,从源码中看出,其操作是在返回值前进行清除的,且清除时进行了加锁保护.
这里需要注意的是,这里键值被GC时不会主动删除这个键值对,而是存在队列中,等待下次操作才进行删除,如果为进行操作,这不会进行删除,此时就像一个HashMap存在,而且在键值存在强引用时,此map就相当于一个普通的HashMap了.

//进行清除时,会调用此方法
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;
                }
            }
        }
    }

小结

java提供了很多的map,而且大部分操作都针对其痛点进行了优化,总的来说,看完这些map中的设计后,还是觉得自己还是太嫩了,虽然有些可能写错了,希望大家指出纠正

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值