Java容器之一 Map总结

JAVA MAP总结

本文内容均来源于网络,总结方便查看面试只用

0 网址

https://blog.csdn.net/jiguquan3839/article/details/84546835 (Java中Map接口的解析)
https://blog.csdn.net/taraex/article/details/90243965
https://www.jianshu.com/p/2fee1d246cad
https://blog.csdn.net/lkforce/article/details/89521318
https://blog.csdn.net/sihai12345/article/details/79383766
https://zhuanlan.zhihu.com/p/104515829
https://www.cnblogs.com/xiayou/p/5735017.html
https://zhuanlan.zhihu.com/p/98589533
http://gityuan.com/2019/01/13/arraymap/

1 Map

原文链接:https://blog.csdn.net/jiguquan3839/article/details/84546835
类图
在这里插入图片描述
给定一个键和一个值,你可以将该值存储在一个Map对象. 之后,你可以通过键来访问对应的值。
当访问的值不存在的时候,方法就会抛出一个NoSuchElementException异常.
当对象的类型和Map里元素类型不兼容的时候,就会抛出一个 ClassCastException异常。
当在不允许使用Null对象的Map中使用Null对象,会抛出一个NullPointerException 异常。
当尝试修改一个只读的Map时,会抛出一个UnsupportedOperationException异常。

1-1 Map的功能概述

转载:
https://blog.csdn.net/taraex/article/details/90243965
https://blog.csdn.net/samniwu/article/details/90550196

a:添加功能
V put(K key,V value):添加元素。这个其实还有另一个功能?替换
如果键是第一次存储,就直接存储元素,返回null
如果键不是第一次存在,就用值把以前的值替换掉, 返 回 以 前 的 值 \color{red}{返回以前的值}
b:删除功能
void clear():移除所有的键值对元素
V remove(Object key):根据键删除键值对元素,并把值返回
c:判断功能
boolean containsKey(Object key):判断集合是否包含指定的键
boolean containsValue(Object value):判断集合是否包含指定的值
boolean isEmpty():判断集合是否为空
d:获取功能
Set<Map.Entry<K,V>> entrySet(): 返回一个键值对的Set集合
V get(Object key):根据键获取值
Set keySet():获取集合中所有键的集合
Collection values():获取集合中所有值的集合
e:长度功能
int size():返回集合中的键值对的对数

2 HashMap

转载
https://blog.csdn.net/jiguquan3839/article/details/84546835
https://blog.csdn.net/samniwu/article/details/90550196

hashMap是由数组和链表这两个结构来存储数据。HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。
HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
在这里插入图片描述
HashMap有4个构造器,其他构造器如果用户没有传入initialCapacity 和loadFactor这两个参数,会使用默认值,initialCapacity默认为16,loadFactory默认为0.75
发生哈希冲突并且size大于阈值的时候,需要进行数组扩容,扩容时,需要新建一个长度为之前数组2倍的新的数组,然后将当前的Entry数组中的元素全部传输过去,扩容后的新数组长度为之前的2倍,所以扩容相对来说是个耗资源的操作。
最终存储位置的确定流程是这样的:
在这里插入图片描述

2-1 重写equals方法需同时重写hashCode方法

源码的get方法会先判断hash值,再比较key值

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        //通过key的hashcode值计算hash值
        int hash = (key == null) ? 0 : hash(key);
        //indexFor (hash&length-1) 获取最终数组索引,然后遍历链表,通过equals方法比对找出对应记录
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && 
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

2-2 HashMap性能问题

转载
https://blog.csdn.net/y1962475006/article/details/78844263

HashMap是可以以null为key的(代码显而易见);
HashMap是非线程安全的(也没看到用到sychronized、volatile和CAS);
HashMap的实际大小总是2的指数幂;
HashMap数组+单链表的数据结构,其实就是用直接链法处理hash冲突(相关概念可以自行百度);
HashMap的大小不是一成不变的,而是在达到阀值(数组大小0.75倍),就开始扩容;
扩容后的大小是原来的两倍;
put方法中,e.hash == hash && key.equals(e.key)是判定是否为同一个实体的条件;
get方法中,eKey == key || (e.hash == hash && key.equals(eKey)是判定是否为同一个实体的条件。

性能问题一,HashMap的大小
为什么说HashMap的实际大小总是2的指数幂?因为就算初始化的时候不是2的指数幂,roundUpToPowerOfTwo函数也会帮你转。为什么一定要是2的指数幂?这个是由于HashMap使用的散列算法,就是用key的hashCode转成对应的二进制,然后和HashMap的size-1做“&”操作。
HashMap的大小过小的时候,会增加Hash冲突的几率;当当前的大小达到阀值(默认0.75*size),就会扩容,容量扩大为原来的两倍,扩容的过程会遍历原来的table,把它的元素重新计算在对应的新table中的位置,最坏时间复杂度为O(n^2);而在hash不冲突的场景下,不需要扩容的话,实际的时间复杂度为O(1)
性能问题2:重写equals和hashCode方法:
首先明确这两者的关系:
A和B对象equals方法返回true,hasCode方法返回值必然一样;
A和B对象hashCode不一样,那么equals方法必须返回false。
A和B对象hashCode一样,不能判定A equals B。
所以equals方法返回true和hasCode方法返回值一样是充分非必要的关系。
性能问题3:多线程问题
HashMap是非线程安全的

解决这个问题有几个方法:
1、使用HashTable;
2、Collections.synchronizedMap处理HashMap;
3、使用ConcurrentHashMap
前两种性能太差,推荐使用第三种,ConcurrentHashMap使用CAS轻量级锁,性能更好。

2-3 HashMap中的Hash算法

转载
https://www.jianshu.com/p/2fee1d246cad
源码:

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

获取索引:
int index = hash & (arrays.length-1);
在这里插入图片描述
hashCode右移16位,正好是32bit的一半。与自己本身做异或操作(相同为0,不同为1)。就是为了混合哈希值的高位和地位,增加低位的随机性。并且混合后的值也变相保持了高位的特征。

2-4 Java Object.hashCode()源码分析

转载
https://blog.csdn.net/changrj6/article/details/100043822

2-5 Hashmap实现原理及扩容机制详解

转载
https://blog.csdn.net/lkforce/article/details/89521318

2-5-1HashMap和红黑树

从JDK1.8开始,在HashMap里面定义了一个常量TREEIFY_THRESHOLD,默认为8。当链表中的节点数量大于TREEIFY_THRESHOLD时,链表将会考虑改为红黑树

2-6 Hash冲突解决方法

链接
https://www.cnblogs.com/jing99/p/6985618.html?utm_source=itdadao&utm_medium=referral
1.开放定址法:

2.链地址法

3.再哈希

4.建立公共溢出区

3ConcurrentHashMap详解

转载
https://blog.csdn.net/sihai12345/article/details/79383766
https://zhuanlan.zhihu.com/p/104515829

HashTable是一个线程安全的类,它使用synchronized来锁住整张Hash表来实现线程安全,即每次锁住整张表让线程独占,相当于所有线程进行读写时都去竞争一把锁,导致效率非常低下。ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,允许多个修改操作并发进行,其关键在于使用了锁分段技术。它使用了多个锁来控制对hash表的不同部分进行的修改。

ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的Hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)。
内部的数据结构为
在这里插入图片描述
ConcurrentHashMap 定位一个元素的过程需要进行两次Hash操作,第一次 Hash 定位到 Segment,第二次 Hash 定位到元素所在的链表的头部

3-1 相关问题

1. ConcurrentHashMap中变量使用final和volatile修饰有什么用呢?
Final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。
使用volatile来保证某个变量内存的改变对其他线程即时可见,在配合CAS可以实现不加锁对并发操作的支持。get操作可以无锁是由于Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。

2.我们可以使用CocurrentHashMap来代替Hashtable吗?
我们知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。它们都可以用于多线程的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。因为ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。

3. ConcurrentHashMap有什么缺陷吗?
ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。坏处是严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分数据。

4. ConcurrentHashMap在JDK 7和8之间的区别

JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档

4 HashTable

转载
https://www.cnblogs.com/xiayou/p/5735017.html
https://zhuanlan.zhihu.com/p/98589533

Hashtable继承Map接口,同样实现一个key-value映射的哈希表。其数据结构同样基于数组加链表,任何非空(non-null)的对象都可作为key或者value。
Hashtable通过initial capacity和load factor两个参数调整性能。通常缺省的load factor 0.75也和hashMap相同。

在这里插入图片描述
要同时覆写equals方法和hashCode方法,而不是只写其中一个。

5 LinkedHashMap

转载
https://blog.csdn.net/a724888/article/details/80290276
https://www.cnblogs.com/whoislcj/p/5552421.html

虽然LinkedHashMap增加了时间和空间上的开销,但是它通过维护一个额外的双向链表保证了迭代顺序。特别地,该迭代顺序可以是插入顺序,也可以是访问顺序。因此,根据链表中元素的顺序可以将LinkedHashMap分为:保持插入顺序的LinkedHashMap和保持访问顺序的LinkedHashMap,其中LinkedHashMap的默认实现是按插入顺序排序的。

HashMap和双向链表的密切配合和分工合作造就了LinkedHashMap。特别需要注意的是,next用于维护HashMap各个桶中的Entry链,before、after用于维护LinkedHashMap的双向链表,虽然它们的作用对象都是Entry,但是各自分离,是两码事儿。
在这里插入图片描述
其中,HashMap与LinkedHashMap的Entry结构示意图如下图所示:
在这里插入图片描述
由于LinkedHashMap是HashMap的子类,所以LinkedHashMap自然会拥有HashMap的所有特性。比如,LinkedHashMap也最多只允许一条Entry的键为Null(多条会覆盖),但允许多条Entry的值为Null。

5-1 LinkedHashMap 与 LRU(Least recently used,最近最少使用)算法

LinkedHashMap重写了HashMap中的recordAccess方法(HashMap中该方法为空),当调用父类的put方法时,在发现key已经存在时,会调用该方法;当调用自己的get方法时,也会调用到该方法。

该方法提供了LRU算法的实现,它将最近使用的Entry放到双向循环链表的尾部。也就是说,当accessOrder为true时,get方法和put方法都会调用recordAccess方法使得最近使用的Entry移到双向链表的末尾;当accessOrder为默认值false时,从源码中可以看出recordAccess方法什么也不会做。

5-2 LinkedHashMap使用:

因为我们这里为了实现LRU算法,排序方式 设置为true 访问顺序排序

    int initialCapacity = 10;//初始化容量
    float loadFactor = 0.75f;//加载因子,一般是 0.75f
    boolean accessOrder = true;//排序方式 false 基于插入顺序  true  基于访问顺序
    Map<String, Integer> map = new LinkedHashMap<>(initialCapacity, loadFactor, accessOrder);

5-3 缓存淘汰算法–LRU算法(java代码实现)

https://blog.csdn.net/wangxilong1991/article/details/70172302

6 ArrayMap

转载
http://gityuan.com/2019/01/13/arraymap/

6-1 数据结构

ArrayMap对象的数据储存格式如图所示:
mHashes是一个记录所有key的hashcode值组成的数组,是从小到大的排序方式;
mArray是一个记录着key-value键值对所组成的数组,是mHashes大小的2倍;
在这里插入图片描述
其中mSize记录着该ArrayMap对象中有多少对数据,执行put()或者append()操作,则mSize会加1,执行remove(),则mSize会减1。mSize往往小于mHashes.length,如果mSize大于或等于mHashes.length,则说明mHashes和mArray需要扩容。

ArrayMap类有两个非常重要的静态成员变量mBaseCache和mTwiceBaseCacheSize,用于ArrayMap所在进程的全局缓存功能:

mBaseCache:用于缓存大小为4的ArrayMap,mBaseCacheSize记录着当前已缓存的数量,超过10个则不再缓存;
mTwiceBaseCacheSize:用于缓存大小为8的ArrayMap,mTwiceBaseCacheSize记录着当前已缓存的数量,超过10个则不再缓存。
为了减少频繁地创建和回收Map对象,ArrayMap采用了两个大小为10的缓存队列来分别保存大小为4和8的Map对象。为了节省内存有更加保守的内存扩张以及内存收缩策略。 接下来分别说说缓存机制和扩容机制。

6-2缓存机制

ArrayMap是专为Android优化而设计的Map对象,使用场景比较高频,很多场景可能起初都是数据很少,为了减少频繁地创建和回收,特意设计了两个缓存池,分别缓存大小为4和8的ArrayMap对象。要理解缓存机制,那就需要看看内存分配(allocArrays)和内存释放(freeArrays)。

freeArrays
在这里插入图片描述
freeArrays()触发时机:

当执行removeAt()移除最后一个元素的情况
当执行clear()清理的情况
当执行ensureCapacity()在当前容量小于预期容量的情况下, 先执行allocArrays,再执行freeArrays
当执行put()在容量满的情况下, 先执行allocArrays, 再执行freeArrays

allocArrays
在这里插入图片描述
allocArrays触发时机:

当执行ArrayMap的构造函数的情况
当执行removeAt()在满足容量收紧机制的情况
当执行ensureCapacity()在当前容量小于预期容量的情况下, 先执行allocArrays,再执行freeArrays
当执行put()在容量满的情况下, 先执行allocArrays, 再执行freeArrays

这里需要注意的是只有大小为4或者8的内存分配才有可能从缓存池取数据,因为freeArrays过程放入缓存池的大小只有4或8,对于其他大小的内存分配则需要创建新的数组。 优化小技巧,对于分配数据不超过8的对象的情况下,一定要创建4或者8大小,否则浪费了缓存机制。比如ArrayMap[7]就是不友好的写法,建议写成ArrayMap[8]。

6-3 扩容机制

容量扩张
当mSize大于或等于mHashes数组长度时则扩容,完成扩容后需要将老的数组拷贝到新分配的数组,并释放老的内存。

当map个数满足条件 osize<4时,则扩容后的大小为4;
当map个数满足条件 4<= osize < 8时,则扩容后的大小为8;
当map个数满足条件 osize>=8时,则扩容后的大小为原来的1.5倍;
可见ArrayMap大小在不断增加的过程,size的取值一般情况依次会是4,8,12,18,27,40,60

容量收紧
当数组内存的大小大于8,且已存储数据的个数mSize小于数组空间大小的1/3的情况下,需要收紧数据的内容容量,分配新的数组,老的内存靠虚拟机自动回收。

如果mSize<=8,则设置新大小为8;
如果mSize> 8,则设置新大小为mSize的1.5倍。
也就是说在数据较大的情况下,当内存使用量不足1/3的情况下,内存数组会收紧50%。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值