上一篇文章:Collection集合及其子接口
上一篇文章分享到特殊的Map集合与映射遍历,本篇文章就继续分享本人的学习:
映射
映射(map)是存储键和值间关联(即,键值对)的对象。给定一个键,可以找到其值。键和值都是对象。键必须唯一,但是值可以重复。
映射围绕两个基本操作:get()和put()。为了将值放入到映射中,使用put(),指定键和值。为了获取值,调用get(),传递键作为变元,值会被返回。具体的实现操作在上一篇结尾时有详细代码。
映射类:
常用的映射类:HashMap、LinkedHashMap、TreeMap。
映射的遍历:
映射不是集合,没有实现Collection接口。因此不能直接遍历映射。原理见下图:
Entry接口是在Map接口的内部定义的,是内部接口。
在Map中存储的元素是键-值对,一个键-值对作为一个整体使用Map.Entry接口表示。Map.Entry定义的方法:
K getKey() | 返回该映射条目的键 |
V getValue() | 返回该映射条目的值 |
V setValue(V v) | 将这个映射条目的值设置为v |
为了遍历映射需要采用变通的方法:获得映射的集合视图。
使用entrySet()方法,返回包含映射中所有元素(键-值对)的Set对象
使用keySet(),返回包含映射中所有键的Set对象。
使用values(),返回包含映射中所有值的Collection对象。
对于这三个集合视图都是基于映射的,修改其中的一个集合会影响其他集合。
Map
是一个映射接口,Map没有继承Collection接口,Map提供key到value的映射。一个Map中不能包含相同的key,每个key只能映射一个value。
Map中常用方法:
- contianskey
- contiansvalue
- isEmpty
- remove
- size
- clear 等等。
Map集合主要分为以下三类:
1,HashMap
- HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
- HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
- HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
构造器其声明如下:
classHashMap<K, V>
HashMap类定义了以下构造方法:
HashMap()
HashMap(intcapacity) //指定容量,默认是16
HashMap(intcapacity, float fillRatio) //充填比率,0.0-1.0之间,默认0.75
HashMap(Map<?extends K, ? extends V> m)
提供的API:
void clear()
Object clone()
boolean containsKey(Object key)
boolean containsValue(Object value)
Set<Entry<K, V>> entrySet()
V get(Object key)
boolean isEmpty()
Set<K> keySet()
V put(K key, V value)
void putAll(Map<? extends K, ? extends V> map)
V remove(Object key)
int size()
Collection<V> values()
继承关系:
从图中可以看出:
(01) HashMap继承于AbstractMap类,实现了Map接口。Map是"key-value键值对"接口,AbstractMap实现了"键值对"的通用函数接口。
(02) HashMap是通过"拉链法"实现的哈希表。它包括几个重要的成员变量:table, size, threshold, loadFactor, modCount。
table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。
size是HashMap的大小,它是HashMap保存的键值对的数量。
threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值="容量*加载因子",当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
loadFactor就是加载因子。
modCount是用来实现fail-fast机制的。
2,LinkedHashMap
LinkedHashMap扩展了Hashmap类,该类在内部使用一个链表维护条目添加到映射中的顺序,从而可以按照插入顺序迭代整个映射。
LinkedHashMap定义了以下构造方法:
LinkedHashMap();
LinkedHashMap(intcapacity);
LinkedHashMap(intcapacity, float fillRatio);
LinkedHashMap(Map<?extends K, ? extends V> m);
3,TreeMap
TreeMap类扩展了AbstractMap类,并实现了NavigableMap接口。该类使用树接口存储条目。TreeMap提供了有序存储键/值对的高效手段,并支持快速检索。应当注意,TreeMap确保其元素以键的升序存储。
TreeMap类定义了以下构造方法:
TreeMap( );
TreeMap(Comparator<?super K> comp); //比较器对象作为参数
TreeMap(Map<? extendsK, ? extends V> m);
TreeMap(SortedMap<K, ?extends V> sm);
下面看一下HashMap集合的一个实现栗子:
System.out.println("遍历方式(映射):用映射的方式遍历,映射为Set集合");
for (Map.Entry<String, String> m : map.entrySet()) {//映射遍历
System.out.println(m.getKey()+","+m.getValue());
}
// Set<Entry<String, String>> it = map.entrySet();//等同于上一种方式(映射遍历)
// for (Entry<String, String> entry : it) {
// System.out.println(entry.getKey()+","+entry.getValue());
// }
System.out.println("---------------------------------");
//等同于上一个方式,映射到Set集合后调用迭代器进行遍历
Set<Entry<String, String>> se = map.entrySet();//映射,调用迭代器遍历
Iterator<Entry<String,String>> it = se.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
拓展
ConcurrentHashMap在这里简单拓展一下,
1、jdk1.7 中的ConcurrrentHashMap
- 在jdk1.7中,ConcurrrentHashMap采用分段锁机制,实现并发的更新操作,底层采用数组 + 链表的存储结构,其核心组件是 静态内部类Segment 和 HashEntry。
- Segment 继承ReentrantLock 用来充当锁的角色,每个Segment 对象守护每个散列映射表的若干个桶。
- HashEntry 是用来封装映射表的键值对。
- 每个桶是由若干个HashEntry 对象链接起来的链表。
一个ConcurrentHashMap实例中包含由若干个Segment对象组成的数组,下面我们通过一个图来演示一下ConcurrentHashMap的结构:
2、在jdk1.8 中已经抛弃了分段锁的机制,采用CAS + 同步机制来保证并发更新的安全,底层采用数组+链表+红黑树的存储结构。JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock + Segment + HashEntry,到 JDK1.8 版本中synchronized + CAS + HashEntry + 红黑树,比较如下:
- JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)。
- JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了。
- JDK1.8使用红黑树来优化链表,对于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档。
- JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,以下几点:
- 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了。
- JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然。在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据。
感谢材料提供:
https://blog.csdn.net/weixin_43352448/article/details/87947105
https://www.cnblogs.com/skywang12345/p/3310835.html
其他相关Map集合的实现栗子:点击这里