1. Java Map
1. Java Map 重要观点
- Java Map接口是Java Collections Framework的成员。但是它不是Collection
- 将键映射到值的对象。一个映射不能包含重复的键;每个键最多只能映射到一个值。(不同的键对应的值可以相等)
- Map 接口提供三种collection 视图,允许以键集、值集或键-值映射关系集的形式查看某个映射的内容。
- Map中某些映射实现可明确保证其自然顺序和定制顺序,如 TreeMap 类;另一些映射实现则不保证任何顺序,如 HashMap 类;还有些类保证添加顺序。
- 某些映射实现对可能包含的键和值有所限制。例如,某些实现禁止 null 键和值,另一些则对其键的类型有限制。
2. Java Map类图
一些最常用的Map实现类是HashMap,LinkedHashMap,TreeMap,SortedMap,HashTable,WeakedHashMap。
Set的实现类都是基于Map来实现的(如,HashSet是通过HashMap实现的,TreeSet是通过TreeMap实现的,LinkedHashSet是通过LinkedHashMap来实现的)。
3. Java Map 方法
void clear() //从此映射中移除所有映射关系(可选操作)。 boolean containsKey(Object key) //如果此映射包含指定键的映射关系,则返回 true。 boolean containsValue(Object value) //如果此映射将一个或多个键映射到指定值,则返回 true。 Set<Map.Entry<K,V>> entrySet() //返回此映射中包含的映射关系的 Set 视图。 boolean equals(Object o) //比较指定的对象与此映射是否相等。 V get(Object key) //返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。 int hashCode() //返回此映射的哈希码值。 boolean isEmpty() //如果此映射未包含键-值映射关系,则返回 true。 Set<K> keySet() //返回此映射中包含的键的 Set 视图。 V put(K key, V value) //将指定的值与此映射中的指定键关联(可选操作)。 void putAll(Map<? extends K,? extends V> m) //从指定映射中将所有映射关系复制到此映射中(可选操作)。 V remove(Object key) //如果存在一个键的映射关系,则将其从此映射中移除(可选操作)。 int size() //返回此映射中的键-值映射关系数。 Collection<V> values() //返回此映射中包含的值的 Collection 视图。
2. HashMap
1. HashMap 结构图
在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
HashMap 继承了AbstractMap,实现了Map<K,V>、Cloneable和Serializable接口!
- 实现了Cloneable接口,即覆盖了函数clone(),实现浅拷贝。
- 实现了Serializable接口,支持序列化,能够通过序列化传输。
2. HashMap 重要特点
- HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。可以存入null键,null值
- 底层实现即 (数组 + 单链表 + 红黑树),HashMap的底层是个Node数组(Node<K,V>[] table),在数组的具体索引位置,如果存在多个节点,则可能是以链表或红黑树的形式存在。Node实现了Map.Entry接口,本质上是一个映射(k-v)
- DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始容量为16,0000 0001 左移4位 0001 0000为16,主干数组的初始容量为16,而且这个数组必须是2的倍数。
- MAXIMUM_CAPACITY = 1 << 30;//最大容量为int的最大值除2
- DEFAULT_LOAD_FACTOR = 0.75f;//默认加载因子为0.75,负载因子可以大于1,即当元素个数超过容量长度的0.75倍时,进行扩容。通过调节负载因子,可使 HashMap 时间和空间复杂度上有不同的表现。当我们调低负载因子时,HashMap 所能容纳的键值对数量变少。扩容时,重新将键值对存储新的桶数组里,键的键之间产生的碰撞会下降,链表长度变短。此时,HashMap 的增删改查等操作的效率将会变高,这里是典型的拿空间换时间。相反,如果增加负载因子(负载因子可以大于1),HashMap 所能容纳的键值对数量变多,空间利用率高,但碰撞率也高。这意味着链表长度变长,效率也随之降低,这种情况是拿时间换空间。
- TREEIFY_THRESHOLD = 8; //阈值,在jdk8中,HashMap处理“碰撞”增加了红黑树这种数据结构,如果主干数组上的链表的长度大于8,链表转化为红黑树 。
- UNTREEIFY_THRESHOLD = 6; //hash表扩容后,如果发现某一个红黑树的长度小于6,则会重新退化为链表
- MIN_TREEIFY_CAPACITY = 64; //当hashmap容量大于64时,链表才能转成红黑树
- threshold;//即触发扩容的阈值,临界值=主干数组容量*负载因子
- 解决冲突,链地址法(也叫拉链法)。jdk1.7中,当冲突时,在冲突的地址上生成一个链表,将冲突的元素的key,通过equals进行比较,相同即覆盖,不同则添加到链表上,此时如果链表过长,效率就会大大降低,查找和添加操作的时间复杂度都为O(n);但是在jdk1.8中如果链表长度大于8,链表就会转化为红黑树,时间复杂度也降为了O(logn),性能得到了很大的优化。
- 非同步,线程不安全,存取速度快(同步封装Map m = Collections.synchronizedMap(new HashMap(...));),在并发场景下使用ConcurrentHashMap来代替。
- 扩容增量:原容量的 1 倍,阈值会变为原来的2倍,如 HashSet的容量为16,一次扩容后是容量为32
- 扩容机制:1. 计算新桶数组的容量 newCap 和新阈值 newThr;2.根据计算出的 newCap 创建新的桶数组,桶数组 table 也是在这里进行初始化的;3.将键值对节点重新映射到新的桶数组里。如果节点是 TreeNode 类型,则需要拆分红黑树。如果是普通节点,则节点按原顺序进行分组。【重新映射红黑树的逻辑和重新映射链表的逻辑基本一致。不同的地方在于,重新映射后,会将红黑树拆分成两条由 TreeNode 组成的链表。如果链表长度小于 UNTREEIFY_THRESHOLD,则将链表转换成普通链表。否则根据条件重新将 TreeNode 链表树化。红黑树中仍然保留了原链表节点顺序。有了这个前提,再将红黑树转成链表就简单多了,仅需将 TreeNode 链表转成 Node 类型的链表即可。】
-
HashMap在触发扩容后,阈值会变为原来的2倍,并且会进行重hash,重hash后索引位置index的节点的新分布位置最多只有两个:原索引位置或原索引+oldCap位置。例如capacity为16,索引位置5的节点扩容后,只可能分布在新报索引位置5和索引位置21(5+16)。导致HashMap扩容后,同一个索引位置的节点重hash最多分布在两个位置的根本原因是:1)table的长度始终为2的n次方;2)索引位置的计算方法为“(table.length - 1) & hash”。HashMap扩容是一个比较耗时的操作,定义HashMap时尽量给个接近的初始容量值。【首次put元素需要进行扩容为默认容量16,阈值16*0.75=12,以后扩容后的table大小变为原来的两倍,接下来就是进行扩容后table的调整:假设扩容前的table大小为2的N次方,有上述put方法解析可知,元素的table索引为其hash值的后N位确定那么扩容后的table大小即为2的N+1次方,则其中元素的table索引为其hash值的后N+1位确定,比原来多了一位因此,table中的元素只有两种情况:元素hash值第N+1位为0:不需要进行位置调整;元素hash值第N+1位为1:调整至原索引的两倍位置;扩容或初始化完成后,resize方法返回新的table。】
- HashMap在JDK1.8之后不再有死循环的问题,JDK1.8之前存在死循环的根本原因是在扩容后同一索引位置的节点顺序会反掉。JDK1.8 重新映射后,两条链表中的节点顺序并未发生变化,还是保持了扩容前的顺序。
- get(key):1.判断表或key是否是null,如果是直接返回null;2.获取key的hash值,计算hash&(table.length-1)得到在链表数组中的位置first=tab[hash&(table.length -1)],判断索引处第一个key与传入key是否相等,如果相等直接返回;3.如果不相等,判断链表是否是红黑二叉树,如果是,直接从树中取值;4.如果不是树,就遍历链表查找。
-
put(key,value)的过程:1. 当桶数组 table 为空或者null时,否则以默认大小resize();2.根据键值key计算hash值得到插入的数组索引i,如果tab[i]==null,直接新建节点添加,否则判断当前数组中处理hash冲突的方式为链表还是红黑树(check第一个节点类型即可),分别处理;3. 查找要插入的键值对已经存在,存在的话根据条件判断是否用新值替换旧值;4.如果不存在,则将键值对链入链表中,并根据链表长度决定是否将链表转为红黑树;5.判断键值对数量是否大于阈值,大于的话则进行扩容操作
-
HasMap的扩容机制resize():构造hash表时,如果不指明初始大小,默认大小为16(即Node数组大小16),如果Node[]数组中的元素达到(填充比*Node.length)重新调整HashMap大小 变为原来2倍大小,扩容很耗时
-
HashMap有threshold属性和loadFactor属性,但是没有capacity属性。初始化时,如果传了初始化容量值,该值是存在threshold变量,并且Node数组是在第一次put时才会进行初始化,初始化时会将此时的threshold值作为新表的capacity值,然后用capacity和loadFactor计算新表的真正threshold值。
-
重写计算hash是通过key的hashCode的高16位和低16位异或后和桶的数量取模得到索引位置,即key.hashcode()^(hashcode>>>16)%length,;好处:1.让高位数据与低位数据进行异或,以此加大低位信息的随机性,变相的让高位数据参与到计算中。2. 可以增加 hash 的复杂度,进而影响 hash 的分布性。这也就是为什么 HashMap 不直接使用键对象原始 hash 的原因了。【在 Java 中,hashCode 方法产生的 hash 是 int 类型,32 位宽。前16位为高位,后16位为低位,所以要右移16位。】
-
当同一个索引位置的节点在增加后达到9个时,并且此时数组的长度大于等于64,则会触发链表节点(Node)转红黑树节点(TreeNode,间接继承Node),转成红黑树节点后,其实链表的结构还存在,通过next属性维持。链表节点转红黑树节点的具体方法为源码中的treeifyBin(Node<K,V>[] tab, int hash)方法。而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容。
-
当同一个索引位置的节点在移除后达到6个时,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点。红黑树节点转链表节点的具体方法为源码中的untreeify(HashMap<K,V> map)方法。
-
保证键的唯一性,需要覆盖hashCode方法,和equals方法。先写hashCode再写equals 1、如果两个对象相同(即用equals比较返回true),那么它们的hashCode值一定要相同;2、如果两个对象的hashCode相同,它们并不一定相同(即用equals比较返回false) 【因为equals()方法只比较两个对象是否相同,相当于==,而不同的对象hashCode()肯定是不同,所以如果我们不是看对象,而只看对象的属性,则要重写这两个方法,如Integer和String他们的equals()方法都是重写过了,都只是比较对象里的内容。使用HashMap,如果key是自定义的类,默认的equal函数的行为可能不能符合我们的要求,就必须重写hashcode()和equals()。】
-
序列化:桶数组 table 被申明为 transient。HashMap 并没有使用默认的序列化机制,而是通过实现
readObject/writeObject
两个方法自定义了序列化的内容。【序列化 talbe 存在着两个问题:1.transient 是表明该数据不参与序列化。因为 HashMap 中的存储数据的数组数据成员中,数组还有很多的空间没有被使用,没有被使用到的空间被序列化没有意义,浪费空间。所以需要手动使用 writeObject() 方法,只序列化实际存储元素的数组。;2.同一个键值对在不同 JVM 下,所处的桶位置可能是不同的,在不同的 JVM 下反序列化 table 可能会发生错误。(HashMap 的get/put/remove
等方法第一步就是根据 hash 找到键所在的桶位置,但如果键没有覆写 hashCode 方法,计算 hash 时最终调用 Object 中的 hashCode 方法。但 Object 中的 hashCode 方法是 native 型的,不同的 JVM 下,可能会有不同的实现,产生的 hash 可能也是不一样的。也就是说同一个键在不同平台下可能会产生不同的 hash,此时再对在同一个 table 继续操作,就会出现问题。)】 - fail-fast机制:HashSet通过iterator()返回的迭代器是fail-fast的。
- 四种遍历方法:map.keySet().iterator();map.entrySet().iterator(); foreach map.keySet(); foreach map.entrySet()
- 注意containsKey方法和containsValue方法。前者直接可以通过key的哈希值将搜索范围定位到指定索引对应的链表,而后者要对哈希数组的每个链表进行搜索。
3. TreeMap
1. TreeMap 结构图
基于红黑树(Red-Black tree)的 NavigableMap
实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator
进行排序,具体取决于使用的构造方法。
此实现为 containsKey、get、put 和 remove 操作提供受保证的 log(n) 时间开销。
TreeMap会自动排序,如果存放的对象不能排序则会报错,所以存放的对象必须指定排序规则。排序规则包括自然排序和客户排序。
①自然排序:TreeMap要添加哪个对象就在哪个对象类上面实现java.lang.Comparable接口,并且重写comparaTo()方法,返回0则表示是同一个对象,否则为不同对象。
②客户排序:建立一个第三方类并实现java.util.Comparator接口。并重写方法。定义集合形式为TreeMap tm = new TreeMap(new 第三方类());
TreeMap继承了AbstractMap,实现了NavigableMap、Cloneable和Serializable接口!
- 继承于AbstractMap,AbstractMap实现了equals和hashcode方法。
- 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如查找与指定目标最匹配项。
- 实现了Cloneable接口,即覆盖了函数clone(),实现浅拷贝。
- 实现了Serializable接口,支持序列化,能够通过序列化传输。
- TreeMap是SortedMap接口的实现类
2. TreeMap 重要特点
- 自平衡红黑二叉树,复杂度为O(log (n))
- key支持2种排序方式:1 key 要实现Comparable接口 ,2 定制比较器 Comparator
- TreeMap是非同步的方法【SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));】。
- 它的iterator 方法返回的迭代器是fail-fast的。
- TreeMap的查询、插入、删除效率均没有HashMap高,一般只有要对key排序时才使用TreeMap。
- TreeMap的key不能为null,而HashMap的key可以为null。
- Entry是红黑数的节点,它包含了红黑数的6个基本组成成分:key(键)、value(值)、left(左孩子)、right(右孩子)、parent(父节点)、color(颜色)。
- TreeSet不支持快速随机遍历,只能通过迭代器进行遍历! 两种遍历方法:Iterator【map.entrySet().iterator(); map.keySet().iterator();map.values();】,forEach
4. LinkedHashMap
1. LinkedHashMap 结构图
LinkedHashMap类:LinkedHashMap正好介于HashMap和TreeMap之间,它也是一个hash表,但它同时维护了一个双链表来记录插入的顺序,基本方法的复杂度为O(1)。
当遍历该集合时候,LinkedHashMap将会以元素的添加顺序访问集合的元素。
2. LinkedHashMap 重要特点
- 继承自HashMap,((数组+链表+红黑树)+双向链表)。
- LinkedHashMap在迭代访问Map中的全部元素时,性能比HashMapt好,但是插入时性能稍微逊色于HashMap。
- 非同步,线程不安全,存取速度快(同步封装Map m = Collections.synchronizedMap(new LinkedHashMap(...));)
- 维护插入顺序,从近期访问最少到近期访问最多的顺序(访问顺序)。这种映射很适合构建 LRU 缓存。
- 此实现可以让客户避免未指定的、由
HashMap
(及Hashtable
)所提供的通常为杂乱无章的排序工作,同时无需增加与TreeMap
相关的成本。使用它可以生成一个与原来顺序相同的映射副本,而与原映射的实现无关。【Map copy = new LinkedHashMap(m);】 - LinkedHashMap的每一个键值对都是通过内部的静态类Entry<K, V>实例化的。这个 Entry<K, V>类继承了HashMap.Entry类。这个静态类增加了两个成员变量,before和after来维护LinkedHasMap元素的插入顺序。这两个成员变量分别指向前一个和后一个元素,这让LinkedHashMap也有类似双向链表的表现。
- 它具有HashMap的所有特性,同样允许key和value为null。
-
LinkedHashMap是如何实现LRU的。首先,当accessOrder为true时,才会开启按访问顺序排序的模式,才能用来实现LRU算法。我们可以看到,无论是put方法还是get方法,都会导致目标Entry成为最近访问的Entry,因此便把该Entry加入到了双向链表的末尾(get方法通过调用recordAccess方法来实现,put方法在覆盖已有key的情况下,也是通过调用recordAccess方法来实现,在插入新的Entry时,则是通过createEntry中的addBefore方法来实现),这样便把最近使用了的Entry放入到了双向链表的后面,多次操作后,双向链表前面的Entry便是最近没有使用的,这样当节点个数满的时候,删除的最前面的Entry(head后面的那个Entry)便是最近最少使用的Entry。
5. HashTable
1. HashTable 结构图
此类实现一个哈希表,该哈希表将键映射到相应的值。任何非 null
对象都可以用作键或值。
为了成功地在哈希表中存储和获取对象,用作键的对象必须实现 hashCode
方法和 equals
方法。
2. HashTable 重要特点
- 实现一个哈希表(数组+链表),该哈希表将键映射到相应的值。任何非
null
对象都可以用作键或值。初始时已经构建了数据结构是Entry类型的数组,Entry源码和hashmap基本元素用的node基本是一样的 - 为了成功地在哈希表中存储和获取对象,用作键的对象必须实现
hashCode
方法和equals
方法。 - 默认加载因子(.75)在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查找某个条目的时间(在大多数 Hashtable 操作中,包括 get 和 put 操作,都反映了这一点)。
- 初始容量主要控制空间消耗与执行
rehash
操作所需要的时间损耗之间的平衡。如果初始容量大于 Hashtable 所包含的最大条目数除以加载因子,则永远 不会发生rehash
操作。但是,将初始容量设置太高可能会浪费空间。如果很多条目要存储在一个Hashtable
中,那么与根据需要执行自动 rehashing 操作来增大表的容量的做法相比,使用足够大的初始容量创建哈希表或许可以更有效地插入条目。 - 由所有类的“collection 视图方法”返回的 collection 的 iterator 方法返回的迭代器都是快速失败 的
- 由 Hashtable 的键和元素方法返回的 Enumeration 不 是快速失败的。(保留是为了兼容)
- 同步的,线程安全的,
- HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
-
Hashtable中key和value都不允许为null,而HashMap中key和value都允许为null(key只能有一个为null,而value则可以有多个为null)。但是如果在Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常,这是JDK的规范规定的。
- Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。【.关于2n+1的扩展,在hashtable选择用取模的方式来进行,那么尽量使用素数、奇数会让结果更加均匀一些,hashmap用2的幂,主要是其还有一个hash过程即二次hash,不是直接用key的hashcode,这个过程打散了数据总体就是一个减少hash冲突,并且找索引效率还要高,实现都是要考量这两因素的】
-
Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值,Hashtable在求hash值对应的位置索引时,用取模运算,而HashMap在求位置索引时,则用与运算,且这里一般先用hash&0x7FFFFFFF后,再对length取模,&0x7FFFFFFF的目的是为了将负的hash值转化为正值,因为hash值有可能为负数,而&0x7FFFFFFF后,只有符号外改变,而后面的位都不变。
- hashtable已经算是废弃了,从实现上看,实际hashmap比hashtable改进良多,不管hash方案,还是结构上多红黑树,唯一缺点是非线程安全。但是hashtable的线程安全机制效率是非常差的,现在能找到非常多的替代方案,比如Collections.synchronizedMap,courrenthashmap等
- 遍历方法: table.entrySet().iterator();table.keySet().iterator();Enumeration enu = table.keys();
4. WeakedHashMap
1. WeakedHashMap 结构图
以弱键 实现的基于哈希表的 Map。在 WeakHashMap 中,当某个键不再正常使用时,将自动移除其条目。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。丢弃某个键时,其条目从映射中有效地移除,因此,该类的行为与其他的 Map 实现有所不同。
2. WeakedHashMap 重要特点
- WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null。
- WeakHashMap和HashMap都是通过"拉链法"实现的散列表。
- modCount是用来实现fail-fast机制的
- queue保存的是“已被GC清除”的“弱引用的键”。
- WeakHashMap是不同步的。可以使用 Collections.synchronizedMap 方法来构造同步的 WeakHashMap。
- 垃圾回收机制通过WeakReference和ReferenceQueue实现的。 WeakHashMap的key是“弱键”,即是WeakReference类型的;ReferenceQueue是一个队列,它会保存被GC回收的“弱键”。在每次get或者put的时候都会调用一个
getTable
的方法,而getTable
里又调用了
清空table中无用键值对。原理如下:新建WeakHashMap,将“键值对”添加到WeakHashMap中。当WeakHashMap中某个“弱引用的key”由于没有再被引用而被GC收回时,在GC回收该“弱键”时,这个“弱键”也同时会被添加到"ReferenceQueue(queue)"中。 当下一次我们需要操作WeakHashMap时,会先同步table和queue。table中保存了全部的键值对,而queue中保存被GC回收的键值对;同步它们,就是删除table中被GC回收的键值对。当我们执行expungeStaleEntries时,就遍历"ReferenceQueue(queue)"中的所有key,然后就在“WeakReference的table”中删除与“ReferenceQueue(queue)中key”对应的键值对。expungeStaleEntries,
-
tomcat在ConcurrentCache是使用ConcurrentHashMap和WeakHashMap做了分代的缓存。在put方法里,在插入一个k-v时,先检查eden缓存的容量是不是超了。没有超就直接放入eden缓存,如果超了则锁定longterm将eden中所有的k-v都放入longterm。再将eden清空并插入k-v。在get方法中,也是优先从eden中找对应的v,如果没有则进入longterm缓存中查找,找到后就加入eden缓存并返回。 经过这样的设计,相对常用的对象都能在eden缓存中找到,不常用(有可能被销毁的对象)的则进入longterm缓存。而longterm的key的实际对象没有其他引用指向它时,gc就会自动回收heap中该弱引用指向的实际对象,弱引用进入引用队列。longterm调用expungeStaleEntries()方法,遍历引用队列中的弱引用,并清除对应的Entry,不会造成内存空间的浪费。
7. EntrySet vs KeySet
1. 遍历
遍历Map,并获取其 <Key, Value> 的方法有两种:
(1)KeySet<KeyType>
(2)EntrySet<KeyType, VlaueType>(性能更好)
EntrySet速度比KeySet快了两倍多点;
- hashmap.entryset,在set集合中存放的是entry对象。而在hashmap中的key 和 value 是存放在entry对象里面的;然后用迭代器,遍历set集合,就可以拿到每一个entry对象;得到entry对象就可以直接从entry拿到value了;
- hashmap.keyset,只是把hashmap中key放到一个set集合中去,还是通过迭代器去遍历,然后再通过 hashmap.get(key)方法拿到value; hashmap.get(key)方法内部调用的是getEntry(key),得到entry,再从entry拿到value;
- entry.getvalue可以直接拿到value,hashmap.get(key)是先得到Entry对象,再通过entry.getvalue去拿,直白点说就是hashmap.get(key)走了一个弯路,所以它慢一些;
- keySet()的速度比entrySet()慢了很多,因为对于keySet其实是遍历了2次,一次是转为iterator,一次就从hashmap中取出key所对于的value。而entryset只是遍历了第一次,他把key和value都放到了entry中,所以就快了
差别在哪里呢? 源码给我们答案了。
public V get(Object key) { if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); }
2. 应用
(1)在需要同时获取Map的<Key, Value>时,EntrySet<KeyType, VlaueType>比KeySet<KeyType>方法要快很多。
(2)如果只需要获取Map的Key,建议使用KeySet<KeyType>方法,因为不需要像EntrySet<KeyType, VlaueType>一样开辟额外的空间存储value值。
(3)如果只需要获取Map的Value,建议使用map.values()方法获取values的集合(Collection)。
(4)由于操作系统内存管理的置换算法(LRU,Least Recently Used,近期最少使用算法),多次遍历速度会逐渐增加(直到寄存器被占满),因为常用数据会从主存被缓存到寄存器中。
3. 底层原理
keySet()方法返回一个引用,这个引用指向了HashMap的一个内部类KeySet类,此内部类继承了AbstractSet,此内部类初始化迭代器产生一个迭代器对象KeyIterator,它继承了HashIterator迭代器,HashIterator迭代器初始化拿到了next指向map中的第一个元素。当使用keySet集合遍历key时,其实是使用迭代器KeyIterator迭代每个节点的key。
entrySet()方法同理。
4. 其他
- 对集合进行的for/in操作,最后会被编译器转化为Iterator操作。但是使用for/in时,Iterator是不可见的,所以如果需要调用Iterator.remove()方法,或其他一些操作, for/in循环就有些力不从心了。
- Java中不存在foreach关键字,foreach是for/in的简称。
- for循环比while循环节约内存空间,因为迭代器在for循环中,循环结束,迭代器属于局部变量,循环结束就消失了,while循环中迭代器对象虽然也是局部变量但是要等方法运行完毕才能在内存中消失
- 当循环次数比较多时,while循环理论上要比for循环要高效,因为for循环比while多一条汇编语句
1 import java.util.Collection; 2 import java.util.HashMap; 3 import java.util.Iterator; 4 import java.util.Map; 5 import java.util.Map.Entry; 6 import java.util.Set; 7 8 public class MapDemo { 9 10 public static Map<Integer, String> map; 11 static { 12 map = new HashMap<Integer, String>(); 13 for(int i=0;i<1000000;i++) { 14 map.put(3*i+1, "China"); 15 map.put(3*i+2, "America"); 16 map.put(3*i+3, "Japan"); 17 } 18 } 19 20 public static void main(String[] args) { 21 System.out.println("map keySet ellipse " + MapKeySetMethod() + " ms"); 22 System.out.println("map entrySet ellipse " + MapEntrySetMethod() + " ms"); 23 //为了排除所谓的缓存带来的干扰,这里再多执行几次 24 System.out.println("map keySet ellipse " + MapKeySetMethod() + " ms"); 25 System.out.println("map entrySet ellipse " + MapEntrySetMethod() + " ms"); 26 System.out.println("map keySet ellipse " + MapKeySetMethod() + " ms"); 27 System.out.println("map entrySet ellipse " + MapEntrySetMethod() + " ms"); 28 System.out.println("map keySet ellipse " + MapKeySetMethod() + " ms"); 29 System.out.println("map entrySet ellipse " + MapEntrySetMethod() + " ms"); 30 31 //当只要获取其value的时候可以这么用 32 Collection<String> values = map.values(); 33 for(String str : values) { 34 //System.out.println(str); 35 } 36 //当只要获取其key的时候可以这么用 37 Collection<Integer> keys = map.keySet(); 38 for(Integer key : keys) { 39 //System.out.println(key); 40 } 41 } 42 43 public static long MapKeySetMethod() { 44 long startTime = System.currentTimeMillis(); 45 Set<Integer> keySet = map.keySet(); 46 Iterator<Integer> iterator = keySet.iterator(); 47 while(iterator.hasNext()) { 48 Integer key = iterator.next(); 49 String value = map.get(key); 50 //System.out.println(key + " = " + value); 51 } 52 long endTime = System.currentTimeMillis(); 53 return endTime-startTime; 54 } 55 56 public static long MapEntrySetMethod() { 57 long startTime = System.currentTimeMillis(); 58 Set<Entry<Integer, String>> entrySet = map.entrySet(); 59 Iterator<Entry<Integer, String>> iterator = entrySet.iterator(); 60 while(iterator.hasNext()) { 61 Entry<Integer, String> entry = iterator.next(); 62 Integer key = entry.getKey(); 63 String value = entry.getValue(); 64 //System.out.println(key + " = " + value); 65 } 66 long endTime = System.currentTimeMillis(); 67 return endTime-startTime; 68 } 69 }
8. ConcurrentSkipListMap(JUC)
1. ConcurrentSkipListMap 结构图
C
2. ConcurrentSkipListMap 重要特点
-
可缩放的并发
ConcurrentNavigableMap
实现。映射可以根据键的自然顺序进行排序,也可以根据创建映射时所提供的Comparator
进行排序,具体取决于使用的构造方法。 -
此类实现 SkipLists 的并发变体,为 containsKey、get、put、remove 操作及其变体提供预期平均 log(n) 时间开销。多个线程可以安全地并发执行插入、移除、更新和访问操作。迭代器是弱一致 的,返回的元素将反映迭代器创建时或创建后某一时刻的映射状态。它们不 抛出
ConcurrentModificationException
,可以并发处理其他操作。升序键排序视图及其迭代器比降序键排序视图及其迭代器更快。 -
此类及此类视图中的方法返回的所有 Map.Entry 对,表示他们产生时的映射关系快照。它们不 支持 Entry.setValue 方法。(注意,根据所需效果,可以使用 put、putIfAbsent 或 replace 更改关联映射中的映射关系。)
-
请注意,与在大多数 collection 中不同,这里的 size 方法不是 一个固定时间 (constant-time) 操作。因为这些映射的异步特性,确定元素的当前数目需要遍历元素。此外,批量操作 putAll、equals 和 clear 并不 保证能以原子方式 (atomically) 执行。例如,与 putAll 操作一起并发操作的迭代器只能查看某些附加元素。
9. ConcurrentHashMap(JUC)
1. ConcurrentHashMap 结构图
2. ConcurrentHashMap 重要特点
-
支持获取的完全并发和更新的所期望可调整并发的哈希表。此类遵守与
Hashtable
相同的功能规范,并且包括对应于 Hashtable 的每个方法的方法版本。不过,尽管所有操作都是线程安全的,但获取操作不 必锁定,并且不 支持以某种防止所有访问的方式锁定整个表。此类可以通过程序完全与 Hashtable 进行互操作,这取决于其线程安全,而与其同步细节无关。 -
获取操作(包括 get)通常不会受阻塞,因此,可能与更新操作交迭(包括 put 和 remove)。获取会影响最近完成的更新操作的结果。对于一些聚合操作,比如 putAll 和 clear,并发获取可能只影响某些条目的插入和移除。类似地,在创建迭代器/枚举时或自此之后,Iterators 和 Enumerations 返回在某一时间点上影响哈希表状态的元素。它们不会抛出
ConcurrentModificationException
。不过,迭代器被设计成每次仅由一个线程使用。 -
这允许通过可选的 concurrencyLevel 构造方法参数(默认值为 16)来引导更新操作之间的并发,该参数用作内部调整大小的一个提示。表是在内部进行分区的,试图允许指示无争用并发更新的数量。因为哈希表中的位置基本上是随意的,所以实际的并发将各不相同。理想情况下,应该选择一个尽可能多地容纳并发修改该表的线程的值。使用一个比所需要的值高很多的值可能会浪费空间和时间,而使用一个显然低很多的值可能导致线程争用。对数量级估计过高或估计过低通常都会带来非常显著的影响。当仅有一个线程将执行修改操作,而其他所有线程都只是执行读取操作时,才认为某个值是合适的。此外,重新调整此类或其他任何种类哈希表的大小都是一个相对较慢的操作,因此,在可能的时候,提供构造方法中期望表大小的估计值是一个好主意。
抄录网址
- 高效编程之HashMap的entryset和keyset比较
- HashMap的keySet()和entrySet()实现原理
- Java 遍历Map的2种方法(KeySet、EntrySet)
- map集合的keySet和entrySet
- Java常见集合的默认大小及扩容机制
- Java集合源码剖析
- java集合系列——Map介绍(七)
- Java集合系列专栏
- http://tool.oschina.net/apidocs/apidoc?api=jdk_7u4
- http://tool.oschina.net/apidocs/apidoc?api=jdk-zh
- Java集合:HashMap详解(JDK 1.8)
- HashMap 源码详细分析(JDK1.8)
- HashMap JDK1.8原理分析
- 【Java集合源码剖析】HashTable源码剖析
- Java 集合系列13之 WeakHashMap详细介绍(源码解析)和使用示例
- WeakHashMap的使用场景
- Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析
- HashMap?面试?我是谁?我在哪
- HashMap并发导致死循环 CurrentHashMap
- 高并发编程系列:ConcurrentHashMap的实现原理(JDK1.7和JDK1.8)
- Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析
- jdk8之HashMap resize方法详解(深入讲解为什么1.8中扩容后的元素新位置为原位置+原数组长度)
- 深入理解 HashMap put 方法(JDK 8逐行剖析)
- HashMap1.8中多线程扩容引起的死循环问题
- 多线程-ConcurrentHashMap(JDK1.8)
- jdk1.6及1.8 HashMap线程安全分析
- HashMap1.8源码分析及线程安全性问题的分析
- 浅谈HashMap与线程安全 (JDK1.8)