1. 概述

本文将介绍更新 HashMap 中指定键对应值的不同方法。首先,我们回顾 Java 8 之前的传统解决方案;然后,探索 Java 8 及更高版本新增的便捷方法。

2. 初始化示例 HashMap

为演示 HashMap 的值更新操作,我们创建一个以水果为键、价格为值的 Map:

Map<String, Double> priceMap = new HashMap<>();
priceMap.put("apple", 2.45);
priceMap.put("grapes", 1.22);

后续示例将基于这个 Map 操作。现在让我们深入了解 HashMap 键值更新的各种方法。

3. Java 8 之前的方案

3.1. put 方法

put 方法既能更新值也能新增条目:若键已存在则更新值,否则新增键值对。

通过两个测试验证其行为:

@Test
public void givenFruitMap_whenPuttingAList_thenHashMapUpdatesAndInsertsValues() {
    Double newValue = 2.11;
    fruitMap.put("apple", newValue);
    fruitMap.put("orange", newValue);
    
    Assertions.assertEquals(newValue, fruitMap.get("apple"));
    Assertions.assertTrue(fruitMap.containsKey("orange"));
    Assertions.assertEquals(newValue, fruitMap.get("orange"));
}
  • apple 已存在 → 值被更新(第一个断言通过)
  • orange 不存在 → 新增条目(后两个断言通过)

3.2. containsKey + put 组合

通过 containsKey 检查键是否存在,再决定是否调用 put 更新值。若键不存在,可选择新增或跳过。

测试用例如下:

@Test
public void givenFruitMap_whenKeyExists_thenValuesUpdated() {
    double newValue = 2.31;
    if (fruitMap.containsKey("apple")) {
        fruitMap.put("apple", newValue);
    }
    
    Assertions.assertEquals(Double.valueOf(newValue), fruitMap.get("apple"));
}

apple 存在,containsKey 返回 true → put 执行更新 → 断言通过。

4. Java 8+ 新特性

4.1. replace 方法

Map 接口提供了两个重载的 replace 方法:

public V replace(K key, V value);
public boolean replace(K key, V oldValue, V newValue);

单参数版本

仅接收键和新值,返回旧值

@Test
public void givenFruitMap_whenReplacingOldValue_thenNewValueSet() {
    double newPrice = 3.22;
    Double applePrice = fruitMap.get("apple");
    
    Double oldValue = fruitMap.replace("apple", newPrice);
    
    Assertions.assertNotNull(oldValue);
    Assertions.assertEquals(oldValue, applePrice);
    Assertions.assertEquals(Double.valueOf(newPrice), fruitMap.get("apple"));
}

⚠️ 踩坑注意:若键不存在会返回 null,这可能导致歧义——无法区分是键不存在还是值为 null。

三参数版本

接收键、旧值和新值,仅当当前值匹配旧值时才更新,返回布尔值表示是否成功:

@Test
public void givenFruitMap_whenReplacingWithRealOldValue_thenNewValueSet() {
    double newPrice = 3.22;
    Double applePrice = fruitMap.get("apple");
    
    boolean isUpdated = fruitMap.replace("apple", applePrice, newPrice);
    
    Assertions.assertTrue(isUpdated);
}

@Test
public void givenFruitMap_whenReplacingWithWrongOldValue_thenNewValueNotSet() {
    double newPrice = 3.22;
    boolean isUpdated = fruitMap.replace("apple", Double.valueOf(0), newPrice);
    
    Assertions.assertFalse(isUpdated);
}
  • 第一个测试:旧值匹配 → 更新成功
  • 第二个测试:旧值不匹配 → 更新失败

4.2. getOrDefault + put 组合

当键不存在时,getOrDefault 提供默认值,配合 put 可安全新增条目,避免 NullPointerException:

@Test
public void givenFruitMap_whenGetOrDefaultUsedWithPut_thenNewEntriesAdded() {
    fruitMap.put("plum", fruitMap.getOrDefault("plum", 2.41));
    
    Assertions.assertTrue(fruitMap.containsKey("plum"));
    Assertions.assertEquals(Double.valueOf(2.41), fruitMap.get("plum"));
}

plum 不存在 → getOrDefault 返回默认值 2.41 → put 新增条目。

4.3. putIfAbsent 方法

功能等同于 getOrDefault + put 组合

  • 键不存在 → 新增条目
  • 键存在 → 不更新值(例外:若原值为 null 则会更新)

测试用例:

@Test
public void givenFruitMap_whenPutIfAbsentUsed_thenNewEntriesAdded() {
    double newValue = 1.78;
    fruitMap.putIfAbsent("apple", newValue);
    fruitMap.putIfAbsent("pear", newValue);
    
    Assertions.assertTrue(fruitMap.containsKey("pear"));
    Assertions.assertNotEquals(Double.valueOf(newValue), fruitMap.get("apple"));
    Assertions.assertEquals(Double.valueOf(newValue), fruitMap.get("pear"));
}
  • apple 已存在 → 值不变
  • pear 不存在 → 新增条目

4.4. compute 方法

基于 BiFunction 更新值:若键存在则应用函数计算新值,否则抛出 NullPointerException:

@Test
public void givenFruitMap_whenComputeUsed_thenValueUpdated() {
    double oldPrice = fruitMap.get("apple");
    BiFunction<Double, Integer, Double> powFunction = (x1, x2) -> Math.pow(x1, x2);
    
    fruitMap.compute("apple", (k, v) -> powFunction.apply(v, 2));
    
    Assertions.assertEquals(
      Double.valueOf(Math.pow(oldPrice, 2)), fruitMap.get("apple"));
    
    Assertions.assertThrows(
      NullPointerException.class, () -> fruitMap.compute("blueberry", (k, v) -> powFunction.apply(v, 2)));
}
  • apple 存在 → 值更新为平方
  • blueberry 不存在 → 抛出 NPE

4.5. computeIfAbsent 方法

键不存在时新增条目:通过函数计算新值,避免 NPE:

@Test
public void givenFruitMap_whenComputeIfAbsentUsed_thenNewEntriesAdded() {
    fruitMap.computeIfAbsent("lemon", k -> Double.valueOf(k.length()));
    
    Assertions.assertTrue(fruitMap.containsKey("lemon"));
    Assertions.assertEquals(Double.valueOf("lemon".length()), fruitMap.get("lemon"));
}

lemon 不存在 → 计算字符串长度作为值 → 新增条目。

4.6. computeIfPresent 方法

仅当键存在时更新值

@Test
public void givenFruitMap_whenComputeIfPresentUsed_thenValuesUpdated() {
    Double oldAppleValue = fruitMap.get("apple");
    BiFunction<Double, Integer, Double> powFunction = (x1, x2) -> Math.pow(x1, x2);
    
    fruitMap.computeIfPresent("apple", (k, v) -> powFunction.apply(v, 2));
    
    Assertions.assertEquals(Double.valueOf(Math.pow(oldAppleValue, 2)), fruitMap.get("apple"));
}

apple 存在 → 值更新为平方。

4.7. merge 方法

键存在时用 BiFunction 更新值,不存在时新增条目(使用方法第二个参数作为默认值):

@Test
public void givenFruitMap_whenMergeUsed_thenNewEntriesAdded() {
    double defaultValue = 1.25;
    BiFunction<Double, Integer, Double> powFunction = (x1, x2) -> Math.pow(x1, x2);
    
    fruitMap.merge("apple", defaultValue, (k, v) -> powFunction.apply(v, 2));
    fruitMap.merge("strawberry", defaultValue, (k, v) -> powFunction.apply(v, 2));
    
    Assertions.assertTrue(fruitMap.containsKey("strawberry"));
    Assertions.assertEquals(Double.valueOf(defaultValue), fruitMap.get("strawberry"));
    Assertions.assertEquals(Double.valueOf(Math.pow(defaultValue, 2)), fruitMap.get("apple"));
}
  • apple 存在 → 值更新为 defaultValue 的平方
  • strawberry 不存在 → 新增条目,值为 defaultValue

5. 总结

本文介绍了 HashMap 键值更新的多种方法:

  • Java 8 前:put 方法、containsKey + put 组合
  • **Java 8+**:replace、getOrDefault + put、putIfAbsent、compute、computeIfAbsent、computeIfPresent、merge

实际开发中,根据场景选择最合适的方法:

  • 简单更新 → putreplace
  • 条件更新 → replace(key, oldVal, newVal)computeIfPresent
  • 安全新增 → putIfAbsentcomputeIfAbsent
  • 复杂计算更新 → computemerge

完整示例代码见 GitHub 仓库


原始标题:Update the Value Associated With a Key in a HashMap | Baeldung