Map接口
map接口的介绍和实现类
-
map接口的继承树
-
Map接口概述
- Map与Collection并列存在。用于保存具有映射关系的数据:key-value
- Map 中的 key 和 value 都可以是任何引用类型的数据
- Map 中的 key 用Set来存放,不允许重复,即同一个 Map 对象所对应的类,须重写hashCode()和equals()方法
- key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value
- Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和Properties。其中,HashMap是 Map 接口使用频率最高的实现类
-
常用方法
-
添加、删除、修改操作:
Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中 void putAll(Map m):将m中的所有key-value对存放到当前map中 Object remove(Object key):移除指定key的key-value对,并返回value void clear():清空当前map中的所有数据
-
元素查询的操作:
Object get(Object key):获取指定key对应的value boolean containsKey(Object key):是否包含指定的key boolean containsValue(Object value):是否包含指定的value int size():返回map中key-value对的个数 boolean isEmpty():判断当前map是否为空 boolean equals(Object obj):判断当前map和参数对象obj是否相等
-
元视图操作的方法:
Set keySet():返回所有key构成的Set集合 Collection values():返回所有value构成的Collection集合 Set entrySet():返回所有key-value对构成的Set集合
-
Map实现类之一:HashMap
1. HashMap是 Map 接口**使用频率最高**的实现类。
2. 允许使用null键和null值,与HashSet一样,不保证映射的顺序。
3. 所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写:equals()和hashCode()
4. 所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类要重写:equals()
5. 一个key-value构成一个entry
6. 所有的entry构成的集合是Set:无序的、不可重复的
7. HashMap **判断两个** **key** **相等的标准**是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
8. HashMap **判断两个** **value****相等的标准**是:两个 value 通过 equals() 方法返回 true。
HashMap键值对key-value的理解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bX6JCBpG-1652193222253)(C:\Users\Think\AppData\Roaming\Typora\typora-user-images\image-20220510195957853.png)]
- key是无序,不可重复的,有Set来存储
- value是无序,可重复的
HashMap的存储结构和底层实现原理
-
JDK 7及以前版本:HashMap是数组+链表结构(即为链地址法)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HJRFVnB4-1652193222261)(C:\Users\Think\AppData\Roaming\Typora\typora-user-images\image-20220510194811859.png)]
----JDK1.7实现原理步骤 Map map = new HashMap<>(); 1,在实例化后,底层创建了一个长度为16的数组 Enity[16] entity map.put(key,value) 1,首先调用key所在类的hashCode()方法,计算出哈希值,然后经过特定的算法,得到key在entity数组中的存放位置 2,如果该位置上没有数据,则直接将key-value添加到该位置上 ---情况1 3,如果该位置上有数据(存在一个或者多个数据),比较当前key哈希值和已存在的一个或者多个数据的key1,keyn的哈希值 1,如果key的哈希值和已经存在的key1,keyn的哈希值都不相同,则直接添加成功 ---情况2 2,如果key的哈希值和已经存在的key1,keyn的哈希值有相同的,则需要调用key所在类的equals(key1,keyn)进行比较 1,如果返回false:则key添加成功 -----情况3 2,如果返回true:则value将替换value1或者valuen 情况1:key按照数组的结构存储 情况2:key按照链表的结构存储 情况3:key按照链表的结构存储 在不断添加元素的过程中(超出临界值并且存放的非空)时,进行扩容,默认的扩容方式:扩容为原来容量的2倍,并且将原来的数据复制过来
--底层结构概述 1,HashMap的内部存储结构其实是数组和链表的结合。当实例化一个HashMap时, 系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量 (Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个 bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。 2,每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引 用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。 而且新添加的元素作为链表的head。 添加元素的过程: 向HashMap中添加entry1(key,value),需要首先计算entry1中key的哈希值(根据 key所在类的hashCode()计算得到),此哈希值经过处理以后,得到在底层Entry[]数 组中要存储的位置i。如果位置i上没有元素,则entry1直接添加成功。如果位置i上 已经存在entry2(或还有链表存在的entry3,entry4),则需要通过循环的方法,依次 比较entry1中key和其他的entry。如果彼此hash值不同,则直接添加成功。如果 hash值相同,继续比较二者是否equals。如果返回值为true,则使用entry1的value 去替换equals为true的entry的value。如果遍历一遍以后,发现所有的equals返回都 为false,则entry1仍可添加成功。 HashMap的扩容 当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的 长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在 HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算 其在新数组中的位置,并放进去,这就是resize。
-
JDK 8版本发布以后:HashMap是数组+链表+红黑树实现。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-znfYMStb-1652193222263)(C:\Users\Think\AppData\Roaming\Typora\typora-user-images\image-20220510194846590.png)]
----JDK1.8实现原理步骤 Map map = new HashMap<>(); 1,在new HashMap<>()的时候,底层没有创建一个默认长度为16的数组 2,jdk1.8底层代码数组名字为 Node[],jdk1.7底层代码数组为Entry[] 3,首次调用put()方法的时候,底层创建了一个长度为16的数组 4,jdk1.8底层结构为:数组+链表+红黑树,jdk1.7底层结构为:数组+链表 5,当数组某个位置上链表的长度大于8并且当前数组的长度大于64的时候,此时链表的结构该为红黑树结构
-
--底层原理概述
HashMap的内部存储结构其实是数组+链表+树的结合。当实例化一个
HashMap时,会初始化initialCapacity和loadFactor,在put第一对映射关系
时,系统会创建一个长度为initialCapacity的Node数组,这个长度在哈希表
中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为
“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查
找bucket中的元素
每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带
一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能
生成一个Node链。也可能是一个一个TreeNode对象,每一个TreeNode对象
可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个
TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。
那么HashMap什么时候进行扩容和树形化呢?
当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数
size)*loadFactor 时 , 就会进行数组扩容 , loadFactor 的默认 值 (DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认
情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中
元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)
的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元
素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知
HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有
达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成
树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,
下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
JDK1.8相较于之前的变化:
1.HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组
2.当首次调用map.put()时,再创建长度为16的数组
3.数组为Node类型,在jdk7中称为Entry类型
4.形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
5.当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置
上的所有key-value对使用红黑树进行存储。
负载因子值的大小,对HashMap有什么影响
-
负载因子的大小决定了HashMap的数据密度。
-
负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降
-
负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间
HashMapJDK8源码分析
-
调用无参构造器
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } //初始化了加载因子:DEFAULT_LOAD_FACTOR=0.75
-
调用put()方法
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
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)//第一次添加元素的时候Node<K,V>[] tab为空 n = (tab = resize()).length;//创建底层Node<K,V>[] tab数组 if ((p = tab[i = (n - 1) & hash]) == null)//直接添加 tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;//新增的元素的hash和equals()相等,直接替换 else if (p instanceof TreeNode)//判断是否为红黑树 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {//如果不为空,并且不为红黑树的情况 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) {//如果下一个位置为空,直接添加到当前节点的next p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //如果当前节点哈希值相等,则退出循环,直接替换 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
概念点:
--DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16 MAXIMUM_CAPACITY : HashMap的最大支持容量,2^30 --DEFAULT_LOAD_FACTOR:HashMap的默认加载因子 TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树 UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值,转化为链表 MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量。(当桶中Node的 数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行 resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。) table:存储元素的数组,总是2的n次幂 entrySet:存储具体元素的集 size:HashMap中存储的键值对的数量 modCount:HashMap扩容和结构改变的次数。 --threshold:扩容的临界值,=容量*填充因子 loadFactor:填充因子