Map集合的用法及源码解析

Java集合类(三)

前言

本章内容为Java集合中的Map集合的常见用法和源码解析~


  1. Map接口实现类的特点和常用方法
  2. Map接口的六大遍历方式
  3. Map小结及HashMap底层源码分析
  4. 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
    29
    package 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);
    }
    }

    image-20230903202307801

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就可以方便我们的遍历。

    image-20230903202314025

    程序演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package 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);

    }
    }

    输出结果:

    image-20230903202320236

    keySet():与Node封装到EntrySet集合一样,不过他是封装到Set集合,利用该方法可以单独遍历Key值

    values:与Node封装到EntrySet集合一样,不过他是封装到Collection集合,利用该方法可以单独遍历Value值

    1
    2
    3
    4
    5
    Set set1 = map.keySet();
    System.out.println(set1);

    Collection values = map.values();
    System.out.println(values);

    运行结果

    image-20230903202325337

Map接口的常用方法

  1. put():添加元素

  2. remove():根据键删除映射关系

  3. get():根据键获取值

  4. size():获取元素的个数

  5. isEmpty():判断元素个数是否为0

  6. clear():清除集合键值对

  7. 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
    56
    package 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;
    }

    @Override
    public String toString() {
    return "Book{" +
    "name='" + name + '\'' +
    ", price=" + price +
    '}';
    }
    }

    image-20230903202332540

Map接口的六大遍历方式

  • 遍历以下Map接口的元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import 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);
    }
    }
  1. 使用keySet()+增强for遍历k-v

    1
    2
    3
    4
    5
    6
    System.out.println("=====第一种遍历方式=====");
    Set keySet = map.keySet();

    for (Object key : keySet) {
    System.out.println(key+"="+map.get(key));
    }

    运行结果

    image-20230903202338785

  2. 使用keySet()+迭代器遍历k-v

    1
    2
    3
    4
    5
    6
    System.out.println("=====第二种遍历方式=====");
    Iterator iterator = keySet.iterator();
    while (iterator.hasNext()) {
    Object next = iterator.next();
    System.out.println(next+"="+map.get(next));
    }

    运行结果

    image-20230903202344106

  3. 使用values()+增强for遍历value(因为value不能映射key,所以只能遍历value)

    1
    2
    3
    4
    5
    System.out.println("=====第三种遍历方式=====");
    Collection values = map.values();
    for (Object value : values) {
    System.out.println(value);
    }

    运行结果

    image-20230903202349163

  4. 使用values()+迭代器遍历

    1
    2
    3
    4
    5
    6
    System.out.println("=====第四种遍历方式=====");
    Iterator iterator1 = values.iterator();
    while (iterator1.hasNext()) {
    Object next = iterator1.next();
    System.out.println(next);
    }

    运行结果

    image-20230903202355067

  5. 使用EntrySet()+增强for遍历

    1
    2
    3
    4
    5
    6
    7
    8
    System.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());
    }

    运行结果

    image-20230903202400634

  6. 使用EntrySet()+迭代器遍历

    1
    2
    3
    4
    5
    6
    7
    System.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());
    }

    运行结果

    image-20230903202404812

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
    44
    class 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;
    }

    @Override
    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
    47
    package 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);
    }
    }


    }
    }

    运行结果:

    image-20230903202413552

    上述题目中,数据的封装情况如下图

    image-20230903202418564

    所以当我们需要取出value值时,我们需要先通过EntrySet取出Entry,然后通过Entry的指向取到HashSet$Node的值。

Map小结及HashMap底层源码分析

Map小结

  1. Map接口的常用实现类:HashMap、Hashtable和Properties

  2. HashMap是Map接口使用频率最高的实现类

  3. HashMap是以key-value对的方式来存储数据的

  4. key不能重复,但是value可以重复,允许使用null键(只能有一个)和null值(可以多个)

  5. 如果添加相同的可以,则会覆盖原来的key-value,等同于修改。(key不会替换,value会替换)

    1
    2
    3
    4
    5
    6
    7
    8
    if (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;
    }
  6. 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的。

  7. HashMap没有实现同步,因此线程是不安全的。方法没有做同步互斥的操作,没有synchronize

HashMap底层机制及源码剖析

  • HashMap底层机制示意图

    image-20230903202425448

    (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
    13
    package 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
    3
    public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    将负载因子(loadFactor)初始化并且创建一个空的table。

    向HashMap集合添加元素,执行put方法

    1
    2
    3
    public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
    }

    通过hash方法计算key的Hash值,然后返回给putVal方法。

    1
    2
    3
    4
    static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    执行putVal方法,程序首先判断table是否为null,如果为null就table的容量扩容到16

    1
    2
    3
    if ((tab = table) == null || (n = tab.length) == 0){
    n = (tab = resize()).length;
    }

    实现扩容的方法为risize()方法。

    继续调试,程序会判断当前hash值对应的table索引位置上是否已经存在值,如果不存在值,就把当前Hash值的结点赋值进去。

    1
    2
    3
    if ((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
    4
    if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k)))){
    e = p;
    }

    然后继续执行,把需要添加的key值对应的value值替换掉原先存在的key值的value值

    1
    2
    3
    4
    5
    6
    7
    if (e != null) { // existing mapping for key
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
    e.value = value;
    afterNodeAccess(e);
    return oldValue;
    }

    返回修改后的value值。

    倘若上述条件不成立,程序会继续进行判断,判断需要添加的结点是否为树结点

    1
    2
    3
    else 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
    14
    else {
    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
    15
    package 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
    27
    class A{
    private int no;

    public A(int no) {
    this.no = no;
    }

    public int getNo() {
    return no;
    }

    @Override
    public int hashCode() {
    return 100;
    }

    public void setNo(int no) {
    this.no = no;
    }

    @Override
    public String toString() {
    return "\nA{" +
    "no=" + no +
    '}';
    }
    }

    运行调试,直至添加到第9个元素,因为达到树化的条件(一条链表的元素个数到达8个),但table的大小<MIN_TREEIFY_CAPACITY(默认是64),所以table会扩容。

    image-20230903202438888

    继续调试,table再次扩容,直至table扩容到容量为64时,该链表会发生树化

    image-20230903202443604

Hashtable

Hashtable的基本介绍

  • Hashtable存放的元素时键值对:即K-V

  • Hashtable的键和值都不能为null,否则会抛出NullPointerException

    1
    2
    3
    if (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
    20
    package 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);
    }
    }

    调试程序,我们可以发现

    image-20230903202449965

  • 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
    20
    private 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.
    @SuppressWarnings("unchecked")
    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
    16
    protected 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);

    image-20230903202457113

HashMap与Hashtable对比

image-20230903202502018

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
    30
    package 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"));
    }
    }

    运行结果

    image-20230903202509619

    需要注意的是,使用getPeoperties时,当put进去的值不是String类型的时候,会返回null

    例如在Properties类中添加以下代码

    1
    2
    properties.put("jack",1);
    System.out.println(properties.getProperty("jack"));

    image-20230903202515034

    阅读getProperty源码,我们可以发现其中原理

    1
    2
    3
    4
    5
    6
    7
    8
    public 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;
    }