一、Map 接口概述

Map 接口是 Java 集合框架中非常重要的一员,位于 java.util 包下。它提供了一种键值对(Key-Value)的存储方式,用于存储具有映射关系的数据,允许通过键(key)来高效地查找对应的值(value)。这种数据结构在编程中非常常见,特别适合需要快速查找和检索的场景。

Map 接口的主要特点

1. 键唯一性

Map 中的每个键(key)都必须是唯一的,不允许出现重复的键。如果向 Map 中放入一个已经存在的键,新的值会覆盖原来的值。

示例代码

Map<String, String> map = new HashMap<>();
map.put("name", "Alice");  // 第一次放入键"name"
map.put("name", "Bob");    // 覆盖键"name"的值
System.out.println(map.get("name")); // 输出: Bob

2. 值可重复

与键不同,值(value)可以重复,多个不同的键可以对应相同的值。

示例代码

Map<String, String> map = new HashMap<>();
map.put("firstName", "John");  // 键"firstName"对应值"John"
map.put("lastName", "John");   // 键"lastName"也对应值"John"
System.out.println(map);       // 输出: {firstName=John, lastName=John}

3. 键值对映射

每个键都精确地映射到一个具体的值,通过键可以快速获取对应的值(时间复杂度通常为O(1))。

示例代码

Map<String, Integer> ageMap = new HashMap<>();
ageMap.put("Alice", 25);
ageMap.put("Bob", 30);
int aliceAge = ageMap.get("Alice");  // 25
int bobAge = ageMap.get("Bob");      // 30

Map 接口的层次结构

Map 接口
├── HashMap(无序,基于哈希表实现,性能最好)
├── TreeMap(有序,基于红黑树实现,按key排序)
├── LinkedHashMap(有序,保持插入顺序)
└── Hashtable(线程安全,但已基本被ConcurrentHashMap取代)

应用场景建议

  1. 快速查找场景:首选HashMap,因其O(1)的平均时间复杂度
  2. 需要排序的场景:选择TreeMap,保证按键排序
  3. 需要保持插入顺序的场景:使用LinkedHashMap
  4. 多线程环境:使用ConcurrentHashMap而非Hashtable
  5. 缓存实现:常使用LinkedHashMap的访问顺序特性实现LRU缓存

LRU缓存示例

final int MAX_ENTRIES = 100;
Map<String, String> lruCache = new LinkedHashMap<String, String>(MAX_ENTRIES, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }
};

二、Map 接口的常用实现类

(一)HashMap

HashMap 是 Map 接口最常用的实现类之一,它基于哈希表实现,具有以下特点:

  1. 存储结构

    • HashMap 内部使用数组和链表(JDK 1.8 及以上版本在链表长度达到一定阈值时会转为红黑树)来存储键值对
    • 哈希表通过哈希函数将键映射到数组的索引位置,从而实现快速的查找、插入和删除操作
    • 示例:当存储键值对("name", "张三")时,会先计算"name"的hashCode(),然后通过哈希算法确定数组索引位置
  2. 性能

    • 查找、插入和删除操作的平均时间复杂度为O(1)
    • 在哈希冲突严重的情况下,性能可能会下降到O(n)
    • 扩容机制:默认初始容量为16,负载因子为0.75,当元素数量超过容量*负载因子时会进行扩容
  3. 线程安全性

    • 非线程安全
    • 多线程环境下如果同时进行修改,可能会导致数据不一致或其他异常
    • 解决方案:可以使用Collections.synchronizedMap()或ConcurrentHashMap
  4. 排序特性

    • 元素是无序的
    • 不保证键值对的存储顺序和遍历顺序一致
    • 应用场景:适合需要快速查找而不关心顺序的场景

(二)TreeMap

TreeMap 基于红黑树实现,它具有以下特点:

  1. 排序性

    • 元素会按照键的自然顺序(实现Comparable接口)或自定义比较器(通过Comparator实现)进行排序
    • 遍历TreeMap时可以得到有序的结果
    • 示例:存储("b",2),("a",1),("c",3)会按照"a","b","c"的顺序遍历
  2. 性能

    • 查找、插入和删除操作的时间复杂度为O(log n)
    • 相比HashMap性能稍低
    • 适合需要排序的场景
  3. 线程安全性

    • 非线程安全
    • 多线程环境下需要额外的同步措施
    • 可以使用Collections.synchronizedSortedMap()包装
  4. 特殊方法

    • 提供firstKey(), lastKey()等方法获取边界元素
    • 提供subMap(), headMap(), tailMap()等方法获取范围视图

(三)LinkedHashMap

LinkedHashMap 是 HashMap 的子类,具有以下特点:

  1. 有序性

    • 在HashMap基础上增加了一个双向链表
    • 可以保证遍历顺序与插入顺序一致(默认情况)
    • 或者按照访问顺序排序(通过构造函数设置accessOrder为true)
    • 应用场景:适合需要保持插入顺序或实现LRU缓存的场景
  2. 性能

    • 性能与HashMap相近
    • 由于需要维护双向链表,内存开销稍大
    • 查找操作时间复杂度仍为O(1)
  3. 线程安全性

    • 非线程安全
    • 多线程环境下需要额外同步
  4. 实现细节

    • 重写了HashMap的节点类,增加了before和after指针
    • 通过重写新节点创建和访问节点的方法来维护链表

(四)Hashtable

Hashtable 是一个古老的 Map 实现类,特点如下:

  1. 线程安全性

    • 是线程安全的
    • 方法大多被synchronized关键字修饰
    • 多线程环境下可以安全使用
    • 但性能相对较低
  2. 存储限制

    • 不允许键或值为null
    • 而HashMap允许键和值为null(但键只能有一个null)
    • 示例:Hashtable.put(null, "value")会抛出NullPointerException
  3. 性能

    • 由于线程安全的开销,性能不如HashMap
    • 在JDK1.0引入,现在一般推荐使用ConcurrentHashMap
  4. 继承体系

    • 是Dictionary类的子类
    • 与HashMap不同,不继承AbstractMap
    • 方法命名风格与早期Java集合类一致

三、Map 接口的核心方法

Map 接口是 Java 集合框架中用于存储键值对的核心接口,它定义了一系列用于操作键值对的方法。Map 中的键必须是唯一的,而值可以重复。以下是对 Map 接口常用方法的详细说明和实际应用场景:

(一)添加元素

V put(K key, V value)

  • 功能:将指定的键值对放入 Map 中
  • 返回值
    • 如果该键已存在,则返回原来的值,并将新值覆盖原来的值
    • 如果键不存在,则返回 null
  • 示例
    Map<String, Integer> map = new HashMap<>();
    map.put("Apple", 10);  // 返回 null
    map.put("Banana", 20); // 返回 null
    Integer oldValue = map.put("Apple", 15); // 返回 10
    

void putAll(Map<? extends K, ? extends V> m)

  • 功能:将另一个 Map 中的所有键值对添加到当前 Map 中
  • 特点:相当于批量 put 操作
  • 示例
    Map<String, Integer> map1 = new HashMap<>();
    map1.put("Apple", 10);
    map1.put("Banana", 20);
    
    Map<String, Integer> map2 = new HashMap<>();
    map2.put("Orange", 30);
    map2.put("Grape", 40);
    
    map1.putAll(map2); // map1 现在包含4个键值对
    

(二)获取元素

V get(Object key)

  • 功能:根据指定的键获取对应的值
  • 返回值:如果键不存在,则返回 null
  • 注意:由于返回 null 可能表示键不存在或值为 null,可以使用 containsKey 方法区分
  • 示例
    Map<String, Integer> map = new HashMap<>();
    map.put("Apple", 10);
    
    Integer value = map.get("Apple"); // 返回 10
    Integer notExist = map.get("Pear"); // 返回 null
    

boolean containsKey(Object key)

  • 功能:判断 Map 中是否包含指定的键
  • 应用场景:在调用 get 方法前先检查键是否存在
  • 示例
    if (map.containsKey("Apple")) {
        // 执行相关操作
    }
    

boolean containsValue(Object value)

  • 功能:判断 Map 中是否包含指定的值
  • 特点:需要遍历所有值,时间复杂度较高
  • 示例
    if (map.containsValue(10)) {
        // 执行相关操作
    }
    

int size()

  • 功能:返回 Map 中键值对的数量
  • 示例
    int count = map.size(); // 返回当前键值对数量
    

boolean isEmpty()

  • 功能:判断 Map 是否为空(即 size 是否为 0)
  • 示例
    if (map.isEmpty()) {
        // Map为空时的处理逻辑
    }
    

(三)删除元素

V remove(Object key)

  • 功能:根据指定的键删除对应的键值对
  • 返回值:返回被删除的值。如果键不存在,则返回 null
  • 示例
    Integer removed = map.remove("Apple"); // 删除键为Apple的键值对
    

void clear()

  • 功能:清空 Map 中的所有键值对
  • 特点:执行后 size 将变为 0
  • 示例
    map.clear(); // 清空整个Map
    

(四)获取集合视图

Set<K> keySet()

  • 功能:返回 Map 中所有键组成的 Set 集合
  • 特点:返回的是视图,对 Map 的修改会反映在返回的 Set 上
  • 应用场景:遍历所有键
  • 示例
    for (String key : map.keySet()) {
        System.out.println(key);
    }
    

Collection<V> values()

  • 功能:返回 Map 中所有值组成的 Collection 集合
  • 特点:返回的是视图,可能包含重复值
  • 示例
    for (Integer value : map.values()) {
        System.out.println(value);
    }
    

Set<Map.Entry<K, V>> entrySet()

  • 功能:返回 Map 中所有键值对(Entry 对象)组成的 Set 集合
  • 特点
    • Entry 是 Map 接口的内部接口
    • Entry 包含 getKey() 和 getValue() 方法用于获取键和值
    • 效率最高的遍历方式
  • 示例
    for (Map.Entry<String, Integer> entry : map.entrySet()) {
        System.out.println(entry.getKey() + ": " + entry.getValue());
    }
    

这些方法构成了 Map 接口的核心功能,为开发人员提供了灵活操作键值对的能力。在实际应用中,根据具体需求选择合适的方法可以大大提高代码的效率和可读性。

四、Map 的遍历方式

遍历 Map 是开发中经常遇到的操作,常见的遍历方式有以下几种:

(一)通过 keySet() 遍历

先获取所有键的集合,然后通过键来获取对应的值。这种方法适用于需要单独处理键或值的场景,比如只需要检查某些键是否存在。示例代码如下:

Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);

// 遍历键集合
for (String key : map.keySet()) {
    // 通过键获取值
    Integer value = map.get(key);
    System.out.println("key: " + key + ", value: " + value);
    
    // 可以在此处对键进行其他操作
    if (key.startsWith("a")) {
        System.out.println("Found key starting with 'a': " + key);
    }
}

这种方式的缺点是当 Map 较大时,通过get(key)获取值可能会有性能损耗(对于 HashMap 影响不大,但对于 TreeMap 等基于树结构的 Map 来说,每次获取值都需要进行一次查找)。在性能敏感的场景下,建议使用entrySet()。

(二)通过 entrySet() 遍历

获取所有键值对的集合,然后直接遍历键值对。这是最推荐的遍历方式,因为它在所有Map实现中都能保持较好的性能。特别适合需要同时处理键和值的场景。示例代码如下:

Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);

// 直接遍历键值对
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    String key = entry.getKey();
    Integer value = entry.getValue();
    System.out.println("key: " + key + ", value: " + value);
    
    // 可以同时访问键值对
    if (value > 1) {
        System.out.println(key + " has value greater than 1");
    }
}

(三)通过 values() 遍历

如果只需要遍历值而不需要键,可以使用这种方式。常见于统计、求和等场景。示例代码如下:

Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);

// 仅遍历值
int sum = 0;
for (Integer value : map.values()) {
    System.out.println("value: " + value);
    sum += value;
}
System.out.println("Total sum: " + sum);

(四)使用迭代器遍历

迭代器遍历可以在遍历过程中安全地进行删除操作,避免了ConcurrentModificationException异常。特别适合需要条件删除的场景。示例代码如下:

Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);

// 使用迭代器遍历
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<String, Integer> entry = iterator.next();
    String key = entry.getKey();
    Integer value = entry.getValue();
    System.out.println("key: " + key + ", value: " + value);
    
    // 安全删除符合条件的元素
    if (value < 2) {
        iterator.remove();
        System.out.println("Removed: " + key);
    }
}

// 验证删除结果
System.out.println("Remaining items: " + map);

(五)Java 8+ 的 forEach 方法

在Java 8及更高版本中,可以使用更简洁的lambda表达式遍历:

Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);

// 使用lambda表达式遍历
map.forEach((key, value) -> {
    System.out.println("key: " + key + ", value: " + value);
    // 可以在此处添加其他处理逻辑
});

五、Map 的排序

(一)TreeMap 的自然排序与自定义排序

TreeMap 是基于红黑树(Red-Black tree)实现的有序映射集合,它默认会按照键的自然顺序(natural ordering)进行排序。这种排序机制要求键必须实现 Comparable 接口。

自然排序示例

  1. String 类型键:按照字典顺序(lexicographical order)排序

    TreeMap<String, Integer> stringMap = new TreeMap<>();
    stringMap.put("zebra", 1);
    stringMap.put("apple", 2);
    stringMap.put("banana", 3);
    // 遍历顺序:apple → banana → zebra
    

  2. Integer 类型键:按照数值大小排序

    TreeMap<Integer, String> intMap = new TreeMap<>();
    intMap.put(100, "century");
    intMap.put(5, "five");
    intMap.put(30, "thirty");
    // 遍历顺序:5 → 30 → 100
    

自定义排序

当需要按照非自然顺序排序时,可以在构造 TreeMap 时传入 Comparator 对象:

// 自定义比较器:按字符串长度降序排序
Comparator<String> lengthComparator = new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        // 先按长度降序,长度相同则按字典序
        int lengthCompare = Integer.compare(s2.length(), s1.length());
        return lengthCompare != 0 ? lengthCompare : s1.compareTo(s2);
    }
};

TreeMap<String, Integer> customSortedMap = new TreeMap<>(lengthComparator);
customSortedMap.put("apple", 1);
customSortedMap.put("banana", 2);
customSortedMap.put("pear", 3);
customSortedMap.put("kiwi", 4);
/*
 * 遍历顺序:
 * banana(6) 
 * apple(5)
 * kiwi(4)
 * pear(4)
 */

(二)HashMap 和 LinkedHashMap 的排序处理

HashMap 的无序性

HashMap 是基于哈希表实现的,不保证任何顺序(既不是插入顺序,也不是自然顺序)。它的迭代顺序可能随着时间变化而变化。

LinkedHashMap 的有序性

LinkedHashMap 默认保持键值对的插入顺序(insertion-order),但这不是排序顺序。构造时可通过参数设置为访问顺序(access-order)。

// 保持插入顺序的LinkedHashMap(默认)
LinkedHashMap<String, Integer> insertionOrderMap = new LinkedHashMap<>();
insertionOrderMap.put("apple", 1);
insertionOrderMap.put("banana", 2);
insertionOrderMap.put("pear", 3);
// 遍历顺序:apple → banana → pear

// 访问顺序的LinkedHashMap(最近最少使用)
LinkedHashMap<String, Integer> accessOrderMap = new LinkedHashMap<>(16, 0.75f, true);
accessOrderMap.put("apple", 1);
accessOrderMap.put("banana", 2);
accessOrderMap.put("pear", 3);
accessOrderMap.get("apple"); // 访问后,apple会移到末尾
// 遍历顺序:banana → pear → apple

排序处理方法

对于需要排序的场景,可以先将Map转换为List再进行排序:

1.按键排序

Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("zebra", 1);
hashMap.put("apple", 2);
hashMap.put("banana", 3);

// 转换为Entry列表
List<Map.Entry<String, Integer>> entryList = new ArrayList<>(hashMap.entrySet());

// 按键的自然顺序排序
Collections.sort(entryList, (e1, e2) -> e1.getKey().compareTo(e2.getKey()));
/*
 * 排序结果:
 * apple=2
 * banana=3
 * zebra=1
 */

2.按值排序

// 按数值降序排序
Collections.sort(entryList, (e1, e2) -> e2.getValue().compareTo(e1.getValue()));
/*
 * 排序结果:
 * banana=3
 * zebra=1
 * apple=2
 */

3.Java 8+ 的流式处理

// 按键排序
hashMap.entrySet().stream()
    .sorted(Map.Entry.comparingByKey())
    .forEach(System.out::println);

// 按值排序
hashMap.entrySet().stream()
    .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
    .forEach(System.out::println);

注意:这种排序方式会生成新的排序结果,但不会改变原始Map的结构。如果需要保持排序结果,可以考虑将排序后的结果存入新的LinkedHashMap中。

六、注意事项

(一)键的选择

重写 hashCode() 和 equals() 方法

在使用自定义对象作为 Map 的键时,必须正确重写 hashCode() 和 equals() 方法,原因如下:

  1. 哈希定位机制:HashMap 等基于哈希表的 Map 实现首先调用键的 hashCode() 方法计算哈希值,然后通过哈希值确定存储位置(桶)
  2. 相等性判断:当发生哈希冲突时,Map 会调用 equals() 方法判断键是否真正相等
  3. 不重写的后果
    • 默认的 hashCode() 方法(Object 类提供)基于对象地址计算
    • 默认的 equals() 方法使用 == 比较对象引用
    • 可能导致相同的逻辑键被当作不同键处理,无法正确查找或删除键值对

示例

class Person {
    private String name;
    private int age;
    
    // 必须重写 hashCode 和 equals
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person other = (Person) obj;
        return age == other.age && Objects.equals(name, other.name);
    }
}

键的不可变性

推荐使用不可变对象作为 Map 的键,原因包括:

  1. 哈希值稳定性:如果键对象被修改,其 hashCode() 值可能改变,导致:

    • 无法通过 get() 方法找到原本存储的值(因为计算出的新哈希值指向不同的桶)
    • 可能造成内存泄漏(旧值无法被访问但仍存在于 Map 中)
  2. 推荐键类型

    • String
    • Integer 等包装类
    • 其他不可变类
    • 如果要使用自定义可变对象,必须确保不会在作为键期间修改其关键字段

不良实践示例

Map<List<String>, String> map = new HashMap<>();
List<String> key = new ArrayList<>();
key.add("a");
map.put(key, "value");
key.add("b"); // 修改了键对象,导致哈希值改变
System.out.println(map.get(key)); // 可能返回null,无法找到原本存储的值

(二)线程安全问题

线程安全概况

  1. 非线程安全实现

    • HashMap
    • TreeMap
    • LinkedHashMap
    • 多线程并发修改可能导致:
      • 数据不一致
      • 无限循环(在JDK7及之前版本的HashMap中可能发生)
      • ConcurrentModificationException
  2. 线程安全解决方案

    • 使用并发容器

      Map<String, String> concurrentMap = new ConcurrentHashMap<>();
      

      • ConcurrentHashMap 特点:
        • 分段锁设计(JDK7)或 CAS + synchronized(JDK8+)
        • 高并发性能优于 Hashtable
        • 不会锁定整个表
    • 使用同步包装器

      Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
      

      • 特点:
        • 所有方法都使用同步块保护
        • 遍历时仍需要手动同步
        • 性能低于 ConcurrentHashMap

最佳实践

  • 优先选择 ConcurrentHashMap
  • 如果只需要读多写少的并发访问,考虑使用 ConcurrentSkipListMap(基于跳表实现)

(三)null 值的处理

各实现类对null的处理策略

Map实现类 允许null键 允许null值 说明
HashMap 只能有一个null键
LinkedHashMap 继承自HashMap
TreeMap 因为需要键可比较
Hashtable 设计如此
ConcurrentHashMap 并发安全限制
IdentityHashMap 特殊实现

注意事项

  1. 使用 TreeMap 时,如果尝试 put null 键会抛出 NullPointerException
  2. 即使允许 null 值,也要注意避免 NPE:
    String value = map.get("nonexistent");
    value.toUpperCase(); // 可能抛出NPE
    

  3. 建议使用 getOrDefault() 方法提供默认值:
    String value = map.getOrDefault("nonexistent", "");
    

(四)遍历过程中修改 Map

安全遍历和修改策略

  1. 常见错误

    Map<String, Integer> map = new HashMap<>();
    map.put("a", 1);
    map.put("b", 2);
    
    for (String key : map.keySet()) {
        if (key.equals("a")) {
            map.remove(key); // 抛出ConcurrentModificationException
        }
    }
    

  2. 正确做法

    • 使用迭代器的 remove() 方法:
      Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
      while (it.hasNext()) {
          Map.Entry<String, Integer> entry = it.next();
          if (entry.getKey().equals("a")) {
              it.remove(); // 安全删除
          }
      }
      

    • Java 8+ 可以使用 removeIf():
      map.entrySet().removeIf(entry -> entry.getKey().equals("a"));
      

    • 如果需要添加元素,可以先收集要添加的键值对,遍历结束后再添加
  3. 并发修改检测机制

    • 大多数集合类使用 modCount 字段记录结构性修改次数
    • 迭代器会检查预期的 modCount 与实际是否一致
    • 不一致则抛出 ConcurrentModificationException

(五)初始容量和加载因子(针对 HashMap)

性能优化参数

  1. 关键参数

    • 初始容量:哈希表创建时的桶数量,默认为16
    • 加载因子:哈希表扩容的阈值比例,默认为0.75
      • 当元素数量达到 capacity * loadFactor 时触发扩容
      • 扩容通常将容量翻倍(newCapacity = oldCapacity << 1)
  2. 设置建议

    • 预估元素数量为n时:
      int initialCapacity = (int) (n / 0.75f) + 1;
      Map<String, String> map = new HashMap<>(initialCapacity);
      

    • 加载因子权衡:
      • 较高值(如0.9):减少内存使用,但增加哈希冲突
      • 较低值(如0.5):减少哈希冲突,但增加内存使用
  3. 扩容开销

    • 需要重新计算所有键的哈希值
    • 重建哈希表结构
    • 在性能敏感场景应避免频繁扩容
  4. 现代JDK优化

    • JDK8+ 在哈希冲突严重时会将链表转为红黑树
    • 优化了哈希算法减少冲突
    • 但合理设置初始容量仍有重要意义

性能测试示例

// 测试不同初始容量对put性能的影响
int elements = 1_000_000;

// 不指定初始容量
long start = System.nanoTime();
Map<Integer, Integer> map1 = new HashMap<>();
for (int i = 0; i < elements; i++) {
    map1.put(i, i);
}
System.out.println("Default: " + (System.nanoTime() - start) / 1_000_000 + " ms");

// 合理设置初始容量
start = System.nanoTime();
Map<Integer, Integer> map2 = new HashMap<>((int)(elements / 0.75f) + 1);
for (int i = 0; i < elements; i++) {
    map2.put(i, i);
}
System.out.println("Optimized: " + (System.nanoTime() - start) / 1_000_000 + " ms");

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐