一些最经常使用的Map实现类是HashMap,LinkedHashMap,TreeMap,SortedMap,HashTable,WeakedHashMap。html
Set的实现类都是基于Map来实现的(如,HashSet是经过HashMap实现的,TreeSet是经过TreeMap实现的,LinkedHashSet是经过LinkedHashMap来实现的)。 java
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 视图。
在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即便用链表处理冲突,同一hash值的链表都存储在一个链表里。可是当位于一个桶中的元素较多,即hash值相等的元素较多时,经过key值依次查找的效率较低。而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减小了查找时间。node
HashMap 继承了AbstractMap,实现了Map<K,V>、Cloneable和Serializable接口!面试
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。】算法
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倍大小,扩容很耗时segmentfault
HashMap有threshold属性和loadFactor属性,可是没有capacity属性。初始化时,若是传了初始化容量值,该值是存在threshold变量,而且Node数组是在第一次put时才会进行初始化,初始化时会将此时的threshold值做为新表的capacity值,而后用capacity和loadFactor计算新表的真正threshold值。api
重写计算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 一、若是两个对象相同(即用equals比较返回true),那么它们的hashCode值必定要相同;二、若是两个对象的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 继续操做,就会出现问题。)】
基于红黑树(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接口!
LinkedHashMap类:LinkedHashMap正好介于HashMap和TreeMap之间,它也是一个hash表,但它同时维护了一个双链表来记录插入的顺序,基本方法的复杂度为O(1)。
当遍历该集合时候,LinkedHashMap将会以元素的添加顺序访问集合的元素。
HashMap
(及 Hashtable
)所提供的一般为杂乱无章的排序工做,同时无需增长与 TreeMap
相关的成本。使用它能够生成一个与原来顺序相同的映射副本,而与原映射的实现无关。【Map copy = new LinkedHashMap(m);】LinkedHashMap是如何实现LRU的。首先,当accessOrder为true时,才会开启按访问顺序排序的模式,才能用来实现LRU算法。咱们能够看到,不管是put方法仍是get方法,都会致使目标Entry成为最近访问的Entry,所以便把该Entry加入到了双向链表的末尾(get方法经过调用recordAccess方法来实现,put方法在覆盖已有key的状况下,也是经过调用recordAccess方法来实现,在插入新的Entry时,则是经过createEntry中的addBefore方法来实现),这样便把最近使用了的Entry放入到了双向链表的后面,屡次操做后,双向链表前面的Entry即是最近没有使用的,这样当节点个数满的时候,删除的最前面的Entry(head后面的那个Entry)即是最近最少使用的Entry。
此类实现一个哈希表,该哈希表将键映射到相应的值。任何非 null
对象均可以用做键或值。
为了成功地在哈希表中存储和获取对象,用做键的对象必须实现 hashCode
方法和 equals
方法。
null
对象均可以用做键或值。初始时已经构建了数据结构是Entry类型的数组,Entry源码和hashmap基本元素用的node基本是同样的hashCode
方法和 equals
方法。rehash
操做所须要的时间损耗之间的平衡。若是初始容量大于 Hashtable 所包含的最大条目数除以加载因子,则永远 不会发生 rehash
操做。可是,将初始容量设置过高可能会浪费空间。若是不少条目要存储在一个 Hashtable
中,那么与根据须要执行自动 rehashing 操做来增大表的容量的作法相比,使用足够大的初始容量建立哈希表或许能够更有效地插入条目。Hashtable中key和value都不容许为null,而HashMap中key和value都容许为null(key只能有一个为null,而value则能够有多个为null)。可是若是在Hashtable中有相似put(null,null)的操做,编译一样能够经过,由于key和value都是Object类型,但运行时会抛出NullPointerException异常,这是JDK的规范规定的。
Hashtable计算hash值,直接用key的hashCode(),而HashMap从新计算了key的hash值,Hashtable在求hash值对应的位置索引时,用取模运算,而HashMap在求位置索引时,则用与运算,且这里通常先用hash&0x7FFFFFFF后,再对length取模,&0x7FFFFFFF的目的是为了将负的hash值转化为正值,由于hash值有可能为负数,而&0x7FFFFFFF后,只有符号外改变,然后面的位都不变。
以弱键 实现的基于哈希表的 Map。在 WeakHashMap 中,当某个键再也不正常使用时,将自动移除其条目。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,而后被回收。丢弃某个键时,其条目从映射中有效地移除,所以,该类的行为与其余的 Map 实现有所不一样。
getTable
的方法,而getTable
里又调用了expungeStaleEntries,
清空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”对应的键值对。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,不会形成内存空间的浪费。
遍历Map,并获取其 <Key, Value> 的方法有两种:
(1)KeySet<KeyType>
(2)EntrySet<KeyType, VlaueType>(性能更好)
EntrySet速度比KeySet快了两倍多点;
差异在哪里呢? 源码给咱们答案了。
public V get(Object key) { if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); }
(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,近期最少使用算法),屡次遍历速度会逐渐增长(直到寄存器被占满),由于经常使用数据会从主存被缓存到寄存器中。
keySet()方法返回一个引用,这个引用指向了HashMap的一个内部类KeySet类,此内部类继承了AbstractSet,此内部类初始化迭代器产生一个迭代器对象KeyIterator,它继承了HashIterator迭代器,HashIterator迭代器初始化拿到了next指向map中的第一个元素。当使用keySet集合遍历key时,实际上是使用迭代器KeyIterator迭代每一个节点的key。
entrySet()方法同理。
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 }
C
可缩放的并发 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 操做一块儿并发操做的迭代器只能查看某些附加元素。
支持获取的彻底并发和更新的所指望可调整并发的哈希表。此类遵照与 Hashtable
相同的功能规范,而且包括对应于 Hashtable 的每一个方法的方法版本。不过,尽管全部操做都是线程安全的,但获取操做不 必锁定,而且不 支持以某种防止全部访问的方式锁定整个表。此类能够经过程序彻底与 Hashtable 进行互操做,这取决于其线程安全,而与其同步细节无关。
获取操做(包括 get)一般不会受阻塞,所以,可能与更新操做交迭(包括 put 和 remove)。获取会影响最近完成的更新操做的结果。对于一些聚合操做,好比 putAll 和 clear,并发获取可能只影响某些条目的插入和移除。相似地,在建立迭代器/枚举时或自此以后,Iterators 和 Enumerations 返回在某一时间点上影响哈希表状态的元素。它们不会抛出 ConcurrentModificationException
。不过,迭代器被设计成每次仅由一个线程使用。
这容许经过可选的 concurrencyLevel 构造方法参数(默认值为 16)来引导更新操做之间的并发,该参数用做内部调整大小的一个提示。表是在内部进行分区的,试图容许指示无争用并发更新的数量。由于哈希表中的位置基本上是随意的,因此实际的并发将各不相同。理想状况下,应该选择一个尽量多地容纳并发修改该表的线程的值。使用一个比所须要的值高不少的值可能会浪费空间和时间,而使用一个显然低不少的值可能致使线程争用。对数量级估计太高或估计太低一般都会带来很是显著的影响。当仅有一个线程将执行修改操做,而其余全部线程都只是执行读取操做时,才认为某个值是合适的。此外,从新调整此类或其余任何种类哈希表的大小都是一个相对较慢的操做,所以,在可能的时候,提供构造方法中指望表大小的估计值是一个好主意。