Map集合的用法及源码解析
Java集合类(三)
前言
本章内容为Java集合中的Map集合的常见用法和源码解析~
- Map接口实现类的特点和常用方法
- Map接口的六大遍历方式
- Map小结及HashMap底层源码分析
- Hashtable基本介绍
Map接口实现类的特点和常用方法
Map接口的特点
注意:这里讲的是Jdk8的Map接口特点
Map与Collection并列存在。用于保存具有映射关系的数据:key-value
Map中的key和value可以是任何引用类型的数据,会封装到HashMao$Node对象中
Map中的key不允许重复,原因和HashSet一样
Map中的value可以重复
Map的key可以为null,value也可以为null,但是key为null的结点只能有一个,value为null的结点可以有多个
常用String类作为Map的key
key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value
代码演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29package com.conllection_.maps;
import java.util.HashMap;
import java.util.Map;
public class Map_ {
public static void main(String[] args) {
Map map = new HashMap();
//key不可以重复,重复的会被等价替换
//value可以重复
map.put("no1","成志恒");//k-v
map.put("no2","张无忌");//k-v
map.put("no1","张三丰");//当有相同的key,就等于等价替换
map.put("no3","成志恒");//k-v
//key只能有一个null
map.put(null,null);
map.put(null,"abc");//等价替换
//value可以多个为null
map.put("no4",null);
map.put("no5",null);//value可以多个为null
map.put(1,"赵敏");
//通过get方法,传入key,会返回对应的value
System.out.println(map.get("no3"));
System.out.println("map = " + map);
}
}
Map接口k-v详解
Map存放数据的key-value示意图,一对k-v是放在一个HashMap$Node中的。因为Node实现了Entry接口,所以有些书上也说一对k-v就是一个Entry。
k-v 数据最后是存放在HashMap$Node node = newNode(hash,key,value,null) 这个对象里面的。
k-v 为了方便程序员的遍历,还会创建EntrySet集合,该集合存放的元素的类型是Entry。而一个Entry对象本身就具有key,value值。所以有EntrySet<Entry<K,V>>,即
1
transient Set<Map.Entry<K,V>> entrySet;
EntrySet中,定义的类型是Map.Entry,但是实际上存放的还是HashMap$Node。原因如下
1
static class Node<K,V> implements Map.Entry<K,V>
由于Map.Entry提供了两个重要的方法:K getKey(); V getValue() ,所以当我们把HashMap$Node对象存放到EntrySet就可以方便我们的遍历。
程序演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package com.conllection_.maps;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapSource {
public static void main(String[] args) {
Map map = new HashMap();
map.put("no1","成志恒");//k-v
map.put("no2","张无忌");//k-v
Set set = map.entrySet();
for (Object obj:set) {
//向下转型
Map.Entry entry = (Map.Entry)obj;
System.out.println(entry.getKey() + "-" + entry.getValue());
}
System.out.println("Map = " + map);
}
}输出结果:
keySet():与Node封装到EntrySet集合一样,不过他是封装到Set集合,利用该方法可以单独遍历Key值
values:与Node封装到EntrySet集合一样,不过他是封装到Collection集合,利用该方法可以单独遍历Value值
1
2
3
4
5Set set1 = map.keySet();
System.out.println(set1);
Collection values = map.values();
System.out.println(values);运行结果
Map接口的常用方法
put():添加元素
remove():根据键删除映射关系
get():根据键获取值
size():获取元素的个数
isEmpty():判断元素个数是否为0
clear():清除集合键值对
containsKey():查找键是否存在
代码演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56package com.conllection_.maps;
import java.util.HashMap;
import java.util.Map;
public class MapMethod {
public static void main(String[] args) {
Map map = new HashMap();
// 1. put():添加元素
map.put("no1",new Book("小王子",100));
map.put("no1","成志恒");
map.put("no2","史森明");
map.put("no3","李元浩");
map.put("no4","李元浩");
map.put(null,"小虎");
map.put("no5",null);
System.out.println("map = " + map);
// 2. remove():根据键删除映射关系
map.remove("no3");
System.out.println("map = " + map);
// 3. get():根据键获取值
System.out.println(map.get("no1"));
// 4. size():获取元素的个数
System.out.println(map.size());
// 5. isEmpty():判断元素个数是否为0
System.out.println(map.isEmpty());
// 6. clear():清除集合键值对
map.clear();
System.out.println(map.isEmpty());
System.out.println(map.size());
// 7. containsKey():查找键是否存在
map.put("no1",new Book("小王子",100));
System.out.println(map.containsKey("no1"));
System.out.println(map.containsKey("no5"));
}
}
class Book{
private String name;
private double price;
public Book(String name, double price) {
this.name = name;
this.price = price;
}
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
Map接口的六大遍历方式
遍历以下Map接口的元素
1
2
3
4
5
6
7
8
9
10
11
12
13import java.util.Set;
public class MapFor {
public static void main(String[] args) {
Map map = new HashMap();
map.put("no1","成志恒");
map.put("no2","史森明");
map.put("no3","李元浩");
map.put("no4","李元浩");
map.put(null,"小虎");
map.put("no5",null);
}
}
使用keySet()+增强for遍历k-v
1
2
3
4
5
6System.out.println("=====第一种遍历方式=====");
Set keySet = map.keySet();
for (Object key : keySet) {
System.out.println(key+"="+map.get(key));
}运行结果
使用keySet()+迭代器遍历k-v
1
2
3
4
5
6System.out.println("=====第二种遍历方式=====");
Iterator iterator = keySet.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next+"="+map.get(next));
}运行结果
使用values()+增强for遍历value(因为value不能映射key,所以只能遍历value)
1
2
3
4
5System.out.println("=====第三种遍历方式=====");
Collection values = map.values();
for (Object value : values) {
System.out.println(value);
}运行结果
使用values()+迭代器遍历
1
2
3
4
5
6System.out.println("=====第四种遍历方式=====");
Iterator iterator1 = values.iterator();
while (iterator1.hasNext()) {
Object next = iterator1.next();
System.out.println(next);
}运行结果
使用EntrySet()+增强for遍历
1
2
3
4
5
6
7
8System.out.println("=====第五种遍历方式=====");
Set entrySet = map.entrySet();
for (Object obj : entrySet) {
//将obj转成Map.Entry
Map.Entry entry = (Map.Entry)obj;
System.out.println(entry.getKey()+"-"+entry.getValue());
}运行结果
使用EntrySet()+迭代器遍历
1
2
3
4
5
6
7System.out.println("=====第六种遍历方式=====");
Iterator iterator2 = entrySet.iterator();
while (iterator2.hasNext()) {
Object next = iterator2.next();
Map.Entry entry = (Map.Entry)next;
System.out.println(entry.getKey()+"-"+entry.getValue());
}运行结果
HashMap练习题
使用HashMap添加三个元素对象,要求:
- 键:员工ID,值:员工对象
- 遍历显示工资>18000的员工(遍历方法两种)
- 员工类:姓名、工资、员工id
员工类Staff代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44class Staff{
private String name;
private double wages;
private int id;
public Staff(String name, double wages, int id) {
this.name = name;
this.wages = wages;
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getWages() {
return wages;
}
public void setWages(double wages) {
this.wages = wages;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String toString() {
return "Staff{" +
"name='" + name + '\'' +
", wages=" + wages +
", id=" + id +
'}';
}
}测试类MapExercise代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47package com.conllection_.maps;
import java.util.*;
public class MapExercise {
public static void main(String[] args) {
Map map = new HashMap();
map.put("01",new Staff("成志恒",19000,01));
map.put("02",new Staff("史森明",20000,02));
map.put("03",new Staff("小虎",17000,03));
map.put("04",new Staff("李元浩",18000,04));
System.out.println("===第一种方式===");
//使用entrySet方法返回映射中包含的映射的 Set 视图
Set entrySet = map.entrySet();
//利用增强for循环遍历
for (Object o : entrySet) {
//向下转型,为了能调用父类的方法
Map.Entry entry = (Map.Entry)o;
//把value对象指向从entry集中取出来的value
Object value = entry.getValue();
//向下转型,这样可以使用Staff对象的getWages方法。
Staff staff = (Staff) value;
if(staff.getWages()>18000){
System.out.println(staff.toString());
}
}
System.out.println("===第二种方式===");
//使用keySet()方法返回封装到Map封装到Set里面的结点的Key值
Set set = map.keySet();
//创建迭代器
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
//获取迭代器中的数据
Object next = iterator.next();
//向下转型,吧staff对象指向从map里面取到的value对象。
Staff staff1 = (Staff) map.get(next);
if (staff1.getWages()>18000){
System.out.println(staff1);
}
}
}
}运行结果:
上述题目中,数据的封装情况如下图
所以当我们需要取出value值时,我们需要先通过EntrySet取出Entry,然后通过Entry的指向取到HashSet$Node的值。
Map小结及HashMap底层源码分析
Map小结
Map接口的常用实现类:HashMap、Hashtable和Properties
HashMap是Map接口使用频率最高的实现类
HashMap是以key-value对的方式来存储数据的
key不能重复,但是value可以重复,允许使用null键(只能有一个)和null值(可以多个)
如果添加相同的可以,则会覆盖原来的key-value,等同于修改。(key不会替换,value会替换)
1
2
3
4
5
6
7
8if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//此处e指向替换前的Node,所以 e.value = value;即可完成替换
e.value = value;
afterNodeAccess(e);
return oldValue;
}与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的。
HashMap没有实现同步,因此线程是不安全的。方法没有做同步互斥的操作,没有synchronize
HashMap底层机制及源码剖析
HashMap底层机制示意图
(key,value)是一个Node实现了Map$Entry<K,V>
HashMap的扩容机制[和HashSet扩容机制相同]
- HashMap底层维护了Node类型的数组table,默认为null
- 当创建对象时,将加载因子(loadfactor)初始化为0.75
- 当添加key-value是,通过key的hash值得到在table的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key和准备加入的key是否相等,如果相等,则直接替换value;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。
- 第一次添加时,会将需要扩容的table容量扩容到16,临界值(threshold)为12
- 如果需要再次扩容,会将需要扩容的table容量扩容到原来的2倍(32),临界值为原来的2倍(24) 依次类推。
- 在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(转换红黑树),否则任然采用数组扩容机制
源码分析
创建MapSource类
1
2
3
4
5
6
7
8
9
10
11
12
13package com.conllection_.maps;
import java.util.HashMap;
import java.util.Map;
public class MapSource {
public static void main(String[] args) {
Map map = new HashMap();
map.put("no1","成志恒");//k-v
map.put("no2","张无忌");//k-v
map.put("no1","张无忌");//k-v
}
}调试程序,执行HashMap的无参构造器,=。
1
2
3public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}将负载因子(loadFactor)初始化并且创建一个空的table。
向HashMap集合添加元素,执行put方法
1
2
3public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}通过hash方法计算key的Hash值,然后返回给putVal方法。
1
2
3
4static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}执行putVal方法,程序首先判断table是否为null,如果为null就table的容量扩容到16
1
2
3if ((tab = table) == null || (n = tab.length) == 0){
n = (tab = resize()).length;
}实现扩容的方法为risize()方法。
继续调试,程序会判断当前hash值对应的table索引位置上是否已经存在值,如果不存在值,就把当前Hash值的结点赋值进去。
1
2
3if ((p = tab[i = (n - 1) & hash]) == null){
tab[i] = newNode(hash, key, value, null);
}然后modCount++(记录修改的次数),size++(记录已添加元素的个数);
倘若当前hash值对应的table索引位置上已存在值,首先会判断当前索引位置上的hash值是否与需要添加的key值对应的hash值相同且满足以下两个条件之一:
- 准备添加的key值与p指向的Node结点的key是同一个对象
- p指向的Node结点的key的equals()和准备假如的key比较后相同。
如果上述条件皆成立,直接让e指向p(p为当前索引位置上的结点),e为辅助变量
1
2
3
4if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))){
e = p;
}然后继续执行,把需要添加的key值对应的value值替换掉原先存在的key值的value值
1
2
3
4
5
6
7if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}返回修改后的value值。
倘若上述条件不成立,程序会继续进行判断,判断需要添加的结点是否为树结点
1
2
3else if (p instanceof TreeNode){
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
}如果是树结点,会执行putTreeVal()方法,把需要添加的树节点添加到红黑树上。
如果不是树节点,程序会继续调试,进入到下一个else。
1
2
3
4
5
6
7
8
9
10
11
12
13
14else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
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;
}
}在此处程序会进入一个死循环,直至找到对应索引位置上的尾结点或者找到key相同的结点才会退出。详细见HashSet底层添加元素源码分析的最后一步。
模拟HashMap触发扩容、树化情况,并debug验证
创建测试类MapSource01
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package com.conllection_.maps;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class MapSource01 {
public static void main(String[] args) {
Map map = new HashMap();
for (int i=0; i<12;i++){
map.put(new A(i),"hello");
}
System.out.println(map);
}
}创建A类,重写HashCode方法,让他们有统一的Hash值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27class A{
private int no;
public A(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public int hashCode() {
return 100;
}
public void setNo(int no) {
this.no = no;
}
public String toString() {
return "\nA{" +
"no=" + no +
'}';
}
}运行调试,直至添加到第9个元素,因为达到树化的条件(一条链表的元素个数到达8个),但table的大小<MIN_TREEIFY_CAPACITY(默认是64),所以table会扩容。
继续调试,table再次扩容,直至table扩容到容量为64时,该链表会发生树化
Hashtable
Hashtable的基本介绍
Hashtable存放的元素时键值对:即K-V
Hashtable的键和值都不能为null,否则会抛出NullPointerException
1
2
3if (value == null) {
throw new NullPointerException();
}Hashtable使用方法基本上和HashMap一样
Hashtable是线程安全的(synchronize),HashMap是线程不安全的
Hashtable底层的简单剖析
底层有数组 Hashtable$Entry[],初始化大小为11
临界值 threshold = 8 (11*0.75)
创建HashtableSource类调试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package com.conllection_.maps;
import java.util.Hashtable;
public class HashtableSource {
public static void main(String[] args) {
Hashtable hashtable = new Hashtable();
hashtable.put("john",100);
hashtable.put("jack",100);
hashtable.put("mary",200);
hashtable.put("tom",300);
hashtable.put("smith",500);
hashtable.put("minster",600);
hashtable.put("chris",200);
hashtable.put("ssm",500);
System.out.println("hashtable = " + hashtable);
}
}调试程序,我们可以发现
Hashtable按照自己的扩容机制进行扩容
继续调试HashtableSource,执行put方法,可以发现该接口添加元素时执行了addEntry方法把添加的K-V封装到Entry
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
//当增加的元素数量大于或等于临界值时,执行rehash方法进行扩容
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}执行rehash方法进行扩容。下面截取部分rehash源码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
//进行扩容
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
//指向扩容后的Entry
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
}我们可以发现,当容量达到临界值时,rehash方法会通过 **int newCapacity = (oldCapacity << 1) + 1;*这条语句进行扩容。所以hashtable第一次扩容后的容量为23(112+1);
HashMap与Hashtable对比
Properties
Properties类继承于Hashtable类并实现了Map接口,也是使用一种键值对的形式来保存数据
他的使用特点和Hashtable类似
Properties还可以用了从xxx.properties文件中,加载数据到Properties类对象,并进行读取和修改
xxx.properties文件通常作为配置文件,详解见Java读取properties配置文件
Properties的简单使用
Properties_类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30package com.conllection_.maps;
import java.util.Properties;
public class Properties_ {
public static void main(String[] args) {
//Properties 继承了Hashtable
//Properties 也是通过k-v存储数据,当然key与value都不能为null
Properties properties = new Properties();
//添加元素
properties.put("jack",1);
properties.put("john",2);
properties.put("tom","2");
System.out.println("properties = "+properties);
//删除元素
properties.remove("jack");
System.out.println("properties = "+properties);
//修改元素
properties.put("jack",3);//替换
System.out.println("properties = "+properties);
//查找元素
System.out.println(properties.get("jack"));
System.out.println(properties.getProperty("tom"));
}
}运行结果
需要注意的是,使用getPeoperties时,当put进去的值不是String类型的时候,会返回null
例如在Properties类中添加以下代码
1
2properties.put("jack",1);
System.out.println(properties.getProperty("jack"));阅读getProperty源码,我们可以发现其中原理
1
2
3
4
5
6
7
8public String getProperty(String key) {
//把oval指向key对应的value
Object oval = super.get(key);
//使用三目运算符,当oval是String类型时,返回强转为String类型的oval,否则为null
String sval = (oval instanceof String) ? (String)oval : null;
//当sval为null,且设定了defaults的值时,返回defaultsValue,否则返回sval
return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
}