一、Map概述
1、Map特点
(1)Map和Collection没有继承关系,是单独的一个分支
(2)Map集合以键值对方式存储 <key,value>
key和value都是引用数据类型,存储的是对象的内存地址
key起到主导地位,value是key的一个附属品
2、Map接口中常用方法
向Map集合中添加键值对---------V put(K,V)
通过key获取value--------------V get(key)
获取map集合中所有key-------Set keySet() 所有的键是一个set集合
获取map集合中键值对的个数----int size()
获取map集合中所有value-----Collection values()
通过key删除键值对------------V remove(key)
清空map集合-----------------void clear()
判断是否包含某个key---------boolean cotainsKey(key)
判断是否包含某个value---------boolean cotainsValue(value)
判断是否是否为空,元素个数为0---------boolean isEmpty()
Map集合转成Set集合--------- Set<Map.Entry<K,V>> entrySet()
将map中的key和value转成一个set集合,set集合中存储的对象为Map.Entry类型对象,对象包含key和value两个属性
3、Map集合的遍历
Map<Integer,String> map=new HashMap<>();
map.put(1,"zs");
map.put(2,"ls");
map.put(3,"ww");
(1)获取所有key,通过遍历key,来遍历value
- 迭代器
//获得所有Key---Set集合
Set<Integer> keys=map.KeySet();
//迭代器遍历
Iterator<Integer> it=keys.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
foreach
//获得所有Key---Set集合
Set<Integer> keys=map.KeySet();
//foreach迭代
for(Integer k:keys){
System.out.println(k+"--->"+map.get(k)); //1--->zs
}
(2)将map集合全部转成set集合,遍历set集合(适合大数据量)
//Map集合--->Set集合(key部分是一个Entry数组)
Set<Map.Entry<Integer,String>,String> set=map.entrySet();
//迭代器遍历
Iterator<Map.Entry<Integer,String>> it=set.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
//foreach遍历
for(Map.Entry<Integer,String> node:set){
System.out.println(node.getKey()+"--->"+node.getValue()); //1=zs
}
二、HashMap
(一)底层结构
1、哈希表=数组+单向链表
2、哈希表是一个数值和单向链表的结合体,融合了两种数据结构的优点
数组:查询快
链表:增删改快
3、HashMap的key value实质是存在Entry中,Entry是hashMap中封装key-value键值对的
此部分来源:(1条消息) hashmap的实现原理 数组 entry_Mar.三月-CSDN博客_entry数组
(二)底层源码
HashMap底层实际是一个一维数组,数组的元素是一个单向链表
(三)底层实现原理
1、概述实现过程
(1)采用hash算法将要存储的对象存储到数组的对应位置,
(2)当元素较多时,容易出现hash冲突,即多个元素需存储到同一个数组下标位置
(3)解决:单向链表,多个元素以链表形式存储在同一个数组下标下
(4)当单向链表节点数大于8时,链表转成红黑树------几率极小,因为会数组扩容
2、hash算法
(1)数组的初始容量值为什么是16?
- 数组初始值是16,则数组下标是0-15
- 15的二进制数-----1111
- 任何多位二进制数和1111进行与操作的值范围均在0-15(多位二进制位数>=4)
(2)hash算法如何将数据放入map中
- 每个键值对的key,都有一个hashCode------多位的二进制数(>4)
- 将key的hashCode和数组容量-1的二进制数进行与操作------得到key的hash值,即数组的下标
key hashCode 1010111010011101
数组容量-1 15 1111
与操作 1101 (相当于取hashCode后四位)
1101------>13,则该键值对存放在数组的下标13位置
3、hash碰撞
(1)当key通过hash算法得到的哈希值相同时------hash碰撞(hash冲突)
(2)解决:链表
- 将同哈希值的key及value以链表形式存储在同下标下
- 新来的相同哈希值的键值对用尾插法存到同下标下的链表中
1.7及之前----头插法------死锁问题(多线程)
1.8及之后----尾插法
- 当链表的节点数>8------链表转换成红黑树(平衡二叉树)
- 由于数组会不断扩容,链表转换成红黑树的概率为千万分之一
4、数组扩容
- 数组扩容因子是0.75, 当数组存储量超过0.75时,数组就会2倍扩容
初始值:16 超过16*0.75=12 >12扩容2倍--->32
- 为什么2倍扩容------因为这样才能得到全1二进制数进行与操作
- 数组扩容:将小数组复制到大容量数组,会2次哈希
5、数组元素迁移---rehash
(1)JDK7
在准备好新的数组后,map会遍历数组的每个链表,然后遍历链表中的每个Entity,
重新计算其hash值(也有可能不计算),找到新数组中的对应位置,
以头插法插入新的链表。
【注】
- 是否要重新计算hash值的条件这里不深入讨论,读者可自行查阅源码。
- 因为是头插法,因此新旧链表的元素位置会发生转置现象。
- 元素迁移的过程中在多线程情境下有可能会触发死循环(无限进行链表反转)。
(2)JDK8
由于数组的容量是以2的幂次方扩容的,
那么一个Entity在扩容时,新的位置要么在原位置,要么在原长度+原位置的位置。
原因如下图:
数组长度变为原来的2倍,表现在二进制上就是多了一个高位参与数组下标确定。
一个元素通过hash转换坐标的方法计算后,恰好出现一个现象:
最高位是0则坐标不变,
最高位是1则坐标变为“10000+原坐标”,即“原长度+原坐标”。
元素迁移部分来源:HashMap的扩容机制 - 知乎
三、HashTable----properties
1、HashTable的底层是哈希表
2、hashtable的key和value都不可以为null(hashmap可以)
3、HashTable是线程安全的,所有方法都带有synchronized关键字,效率较低
4、HashTable的初始化容量是11
集合扩容是:原容量*2+1
5、properties----继承HashTable,线程安全
(1)properties的存取
Properties p=new Properties ();
//存 setProperty(key,value)
p.setProperty("username","root");
p.setProperty("password","123");
//取 getProperty(key)
String uname=p.getProperty("username");
String pwd=p.getProperty("password");
(2)常见使用:配置文件 xx.properties
反射机制 灵活存取不同对象class信息
四、SoretMap----TreeMap
TreeMap底层是二叉树,可排序Map-----TreeSet(详见TreeSet)
附:老杜总结图