Skip to content

Map 接口


⭐ 体系图

基本介绍

(1)Map 是双列集合,元素形式为 Key - Value(键值对)

(2) key 和 value 可以是任何引用类型的数据,通常会封装到 HashMap$Node 对象中(Node 实现了 Entry 接口)

(3) key 不允许重复(HashSet 的底层是 HashMap),但是 value 可以重复

(4) key 可以为 null,value 也可以为 null

⚠️ 注意:key 为 null,只能有一个键值对

(5)常用 String 类型作为 Map 的 key,key 和 value 之间存在单向一对一关系,即通过指定的 key 能找到对应的 value(get()方法)

常用方法

方法描述
size()返回 Map 中键值对的数量
isEmpty()检查 Map 是否为空,若为空返回 true,否则返回 false
clear()清空 Map 中的所有键值对
put(K key, V value)将指定的键和值添加到 Map 中,若键已存在则更新对应的值
get(Object key)根据指定的键返回对应的值,若键不存在则返回 null。
getOrDefault(Object key, V defaultValue)若键存在则返回对应值,否则返回默认值
remove(Object key)删除指定的键及其对应的值(通过键值删除键值对),返回被删除的值,若键不存在则返回 null
entrySet()返回 Map 中所有键值对的 Set 集合,每个元素为 Map.Entry 对象
keySet()返回 Map 中所有键的 Set 集合
values()返回 Map 中所有值的 Collection 集合
containsKey(Object key)检查 Map 是否包含指定的键,返回 true 或 false
containsValue(Object value)检查 Map 是否包含指定的值,返回 true 或 false
replace(K key, V value)指定键替换对应的 value 值,若键不存在,则不做任何操作
replaceALL((k,v) -> 新的 value 值 )(k,v) ->这部分是固定写法,把所有的键值对中的 oldvalue 替换成新的 newvalue,可以写表达式

⭐Map 接口底层

(1)每一对键值对会存储在 HashMap 的内部 table 数组 中(table 数组的类型是 HashMap$Node [ ] )。table 数组中的每个位置存储的是一个 Node 对象

1. HashMap $ Node 是 Node 的全名,表示它是 HashMap 类的一个内部类 。在 Java 中,$ 是用于区分内部类和外部类的符号

(2)每个 Node 对象封装了四个属性

key、value、hash、next

(3)HashMap$Node implements Map.Entry ,这个接口提供了 getKey()和 getValue()方法,用于遍历 Map 接口中的元素

EntrySet

结构视图

基本介绍

(1)entrySet 是 Map 接口的一个方法,返回一个 Set<Map.Entry<K,V>> 集合

集合的名字叫做 EntrySet

集合的类型是 Set

集合中每个元素的类型是 Map.Entry

每个 Entry 对象封装了一个键值对(Key - Value)

(2) EntrySet 集合中每个 Entry 元素中的 key 被封装成 KeySet 集合(类型是 Set)

(3)EntrySet 集合中每个 Entry 元素中的 value 被封装成 Values 集合(类型是 Collection)

(4) 最终通过引用把 table 数组中存储的每个 Node 节点中的 key 和 value 指向 KeySet 集合和 Values 集合

entry 对象的运行类型是 HashMap$Node

键值对 Key - Value 是存储在 table 数组 Node 节点中,这里只是作为引用传递

遍历方式

基本介绍

(1)entrySet():提取每一对键值对

map.entrySet()方法返回的是一个 Set<Map.Entry<K, V>> 类型的集合,包含多个 Map.Entry 对象,每个 Entry 对象封装了一个键值对(Key - Value)

(2) keySet() 方法:提取所有的 key

(3) values() 方法:提取所有的 value

遍历 key

思路分析

首先通过 map.keySet()方法提取键值对中的键值

然后通过遍历 keySet 数组获取键值对中的键值

方法一:增强 for 循环

java
public class pra {
    public static void main(String[] args) {
        Map map = new HashMap();

        map.put("1", "jack");
        map.put("2", "tom");
        map.put("3", "lucy");

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

键:1
键:2
键:3

方法二:Iterator 迭代器遍历

java
public class pra {
    public static void main(String[] args) {
        Map map = new HashMap();

        map.put("1", "jack");
        map.put("2", "tom");
        map.put("3", "lucy");

        Set set = map.keySet();
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object key =  iterator.next();
            System.out.println("键:" + key + " --> " + "值:" + map.get(key));
        }
    }
}

// 输出结果
键:1
键:2
键:3

遍历 value

思路分析

首先通过 map.values()方法提取键值对中的值

然后通过遍历 values 数组获取键值对中的值

方法一:增强 for 循环

java
public class pra {
    public static void main(String[] args) {
        Map map = new HashMap();

        map.put("1", "jack");
        map.put("2", "tom");
        map.put("3", "lucy");

        Collection set = map.values();
        for (Object values :set) {
            System.out.println("值:" + values);
        }
    }
}

// 输出结果
值:jack
值:tom
值:lucy

方法二:迭代器

java
public class pra {
    public static void main(String[] args) {
        Map map = new HashMap();

        map.put("1", "jack");
        map.put("2", "tom");
        map.put("3", "lucy");

        Collection set = map.values();
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object values =  iterator.next();
            System.out.println("值:" + values);
        }
    }
}
// 输出结果
值:jack
值:tom
值:lucy

遍历 key - value

思路一

Map 接口提供了 get()方法,可以通过遍历键,之后调用 get()方法获取键值对对应的值

方法一:增强 for 循环

java
public class pra {
    public static void main(String[] args) {
        Map map = new HashMap();

        map.put("1", "jack");
        map.put("2", "tom");
        map.put("3", "lucy");

        Set set = map.keySet();
        for (Object key : map.keySet()) {
            System.out.println("键:" + key + " --> " + "值:" + map.get(key));
        }
    }
}

// 输出结果
键:1 --> 值:jack
键:2 --> 值:tom
键:3 --> 值:lucy

方法二:Iterator 迭代器遍历

java
public class pra {
    public static void main(String[] args) {
        Map map = new HashMap();

        map.put("1", "jack");
        map.put("2", "tom");
        map.put("3", "lucy");

        Set set = map.keySet();
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object key =  iterator.next();
            System.out.println("键:" + key + " --> " + "值:" + map.get(key));
        }
    }
}

// 输出结果
键:1 --> 值:jack
键:2 --> 值:tom
键:3 --> 值:lucy

思路二

通过 entryset()方法,获取 entry 对象()(类型是 Map . entry),通过 getKey()和 getValues()方法(需要向下转型为 Map . entry 类型的对象后调用)获取键和值

方法一:增强 for 循环

java
public class pra {
    public static void main(String[] args) {
        Map map = new HashMap();

        map.put("1", "jack");
        map.put("2", "tom");
        map.put("3", "lucy");

        Set set = map.entrySet();
        for (Object obj :set) {
            // 向下转型
            Map.Entry entry =(Map.Entry) obj;
            System.out.println("键:" + entry.getKey() + " --> 值:" + entry.getValue());
        }
    }
}

// 输出结果
键:1 --> 值:jack
键:2 --> 值:tom
键:3 --> 值:lucy

方法二:迭代器

java

public class pra {
    public static void main(String[] args) {
        Map map = new HashMap();

        map.put("1", "jack");
        map.put("2", "tom");
        map.put("3", "lucy");

        Set set = map.entrySet();
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object obj =  iterator.next();
            Map.Entry entry = (Map.Entry)obj;
            System.out.println("键:" + entry.getKey() + " --> 值:" + entry.getValue());
        }
    }
}

HashMap

基本介绍

(1)HashMap 是 Map 接口中使用频率最高的实现类

(2)HashMap 是以 key - value 对的方式存储数据的(HashMap$Node 类型

(3)key 不能重复,value 可以重复,允许使用 null 键和 null 值,key 的 null 值只能有一个

(4)添加的 key - value 中如果key 相同,value 会被覆盖(因为 key 不能重复)

(5)与 HashSet 一样, 不保证映射的顺序, 因为底层是以 hash 表的方式来存储的.

(6)jdk8 的 HashMap 底层是数组 + 链表 + 红黑树

(7)HashMap 是线程不安全

⭐ 底层源码

关于 HashMap 的底层添加机制和扩容机制在 Set 接口中的 HashSet 中已经分析,这里重点看一下如果添加键值相同时,value 是如何被替换的

源码如下

当添加相同元素时,e 不为空,进入如下分支判断

前面的源码可以参考 Set 接口的笔记,其中对 HashMap 底层源码的分析

java
if (e != null) { // existing mapping for key
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
        e.value = value;
    afterNodeAccess(e);
    return oldValue;
}

代码分析

(1)在 HashSet 中,底层调用的其实是 HashMap,但是由于 HashSet 是单列集合,HashMap 是双列集合,调用 put()方法会传入两个值,value 由 PRESENT 代替

(2)HashSet 中的 e.value 就是 PRSENT,在 HashMap 中 PRESENT 的值变成了传入的 value,即 e . value 表示传入的 value

(3)V oldValue = e.value,这一步实现了新的 value 覆盖旧的 value

Hashtable

基本介绍

(1)存放的元素是键值对:即 K-V

(2) Hashtable 的键和值都不能为 null,否则会抛出 NullPointerException 异常

(3) Hashtable 使用方法基本和 HashMap 一样

(4) Hashtable线程安全的 (synchronized)

对比 HashMap

类型版本线程安全(同步)效率允许 null 键 null 值
HashMap1.2不安全可以
Hashtable1.0安全不可以

Properties

(1) Properties 类继承自 Hashtable 类,并且实现了 Map 接口,也是使用一种键值对(Key - Value)的形式来存储数据

(2) Properties 是线程安全

(3) Properties 可以从 xxx.properties 配置文件中加载数据,通过 Properties 对象对配置文件信息进行读取和修改

⚠️ 提醒:Properties 主要应用在文件操作,相关使用和相关方法请查看 Java 第二阶段 IO 流部分的笔记

TreeMap

基本介绍

TreeMap 提供了一个构造器,可以传入 Comparator 比较器来指定添加的顺序

⚠️TreeSet 的底层是 TreeMap,更多相关介绍可以查看 Set 接口中关于 TreeSet 的笔记

⭐ 源码分析

区别于 TreeSet,TreeMap 是双列集合,不会进入 add()方法,而是直接进入 put()方法

进入 put()方法

java
public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) {
        compare(key, key); // type (and possibly null) check

        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    else {
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

第一次添加

第一次添加,把 k - v 键值对封装到 Entry 对象中

java
Entry<K,V> t = root;
if (t == null) {
    compare(key, key); // type (and possibly null) check

    root = new Entry<>(key, value, null);
    size = 1;
    modCount++;
    return null;
}

之后的添加

java
Comparable<? super K> k = (Comparable<? super K>) key;
do {
    parent = t;
    cmp = k.compareTo(t.key);
    if (cmp < 0)
        t = t.left;
    else if (cmp > 0)
        t = t.right;
    else
        return t.setValue(value);
} while (t != null);

代码分析

(1)遍历所有的 key,给当前的 key - value 键值对找到插入位置

(2)调用传入的比较器,实现 compare 方法(动态绑定机制

(3)else 分支:在遍历过程中,如果发现当前的 key 和添加的 key 一样,就不会执行添加操作

⭐ 面试经典易错题

下面代码中的 java 是否会被添加到 TreeMap 中

java
public class pra {
    public static void main(String[] args) {

        TreeMap treeMap = new TreeMap(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((String)o1).length() - ((String)o2).length();
            }
        });

        treeMap.put("jack","1");
        treeMap.put("tom","2");
        treeMap.put("hi","3");
        treeMap.put("java","4");

        System.out.println("treeSet = " + treeMap);
    }
}

// 输出结果
treeSet = {hi=3, tom=2, jack=4}

答案:不会,当添加的 Key 相同时,会对 Value 进行更新

底层源码

java
do {
    parent = t;
    cmp = k.compareTo(t.key);
    if (cmp < 0)
        t = t.left;
    else if (cmp > 0)
        t = t.right;
    else
        return t.setValue(value);
} while (t != null);

代码分析

(1)传入的比较器是比较字符串的长度,字符串的长度相同时返回 0,进入 else 分支

(2)调用 setValue()方法,更新 key 对应的 value,返回这个 value