更新与HashMap中的键关联的值

2023/06/07

1. 概述

本教程将介绍更新与HashMap中给定键关联的值的不同方法。首先,我们将查看一些仅使用Java 8之前可用的功能的常见解决方案。然后,我们将介绍Java 8及更高版本中可用的一些其他解决方案。

2. 初始化我们的示例HashMap

首先,我们将创建一个Map,其中水果名作为键,价格作为值:

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

3. Java 8之前

让我们从Java 8之前可用的方法开始。

3.1 put方法

put方法更新值或添加新条目。如果它与已存在的键一起使用,则put方法将更新关联的值。否则,它将添加一个新的(key, value)对。

让我们用两个简单的例子来测试这个方法的行为:

@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键已经在Map上了。因此,第一个断言将通过。

由于Map中不存在orange,put方法将添加它。因此,其他两个断言也将通过。

3.2 containsKey和put方法的组合

containsKey和put方法的组合是更新HashMap中键值的另一种方法。此选项检查Map是否已包含键。在这种情况下,我们可以使用put方法更新值。否则,我们要么向Map添加一个条目,要么什么都不做。

在我们的例子中,我们将通过一个简单的测试来检查这种方法:

@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在Map上,containsKey方法将返回true。因此,将执行对put方法的调用,并更新值。

4. Java 8及以上版本

从Java 8开始,有许多新方法可以用来简化更新HashMap中键值的过程。

4.1 replace方法

从版本8开始,Map接口中提供了两个重载的replace方法。让我们看一下方法签名:

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

第一个replace方法只需要一个键和一个新值,它还返回旧值

让我们看看该方法是如何工作的:

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

apple键的值将使用replace方法更新为新价格。因此,第二个和第三个断言将通过。

然而,第一个断言很有趣。如果我们的HashMap中没有apple键怎么办?如果我们尝试更新不存在的键的值,将返回null。考虑到这一点,另一个问题出现了:如果有一个空值的键怎么办?我们不知道从replace方法返回的值是否确实是提供的键的值,或者我们是否尝试更新不存在的键的值。

所以,为了避免误会,我们可以使用第二种replace方法。它需要三个参数:

  • 与键关联的当前值
  • 与键关联的新值

它会在一种情况下将键的值更新为新值:如果第二个参数是当前值,则键值将更新为新值。该方法返回true表示更新成功。否则,返回false

因此,让我们实现一些测试来检查第二个replace方法:

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

由于第一个测试使用键的当前值调用replace方法,因此该值将被替换。

另一方面,第二个测试不使用当前值调用。因此,返回false。

4.2 getOrDefault和put方法的组合

如果我们没有提供的键的条目,则getOrDefault方法是一个完美的选择。在这种情况下,我们为不存在的键设置默认值。然后,条目被添加到Map中。使用这种方法,我们可以很容易地摆脱NullPointerException

让我们用一个原本不在Map上的键来尝试这个组合:

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

由于没有这样的键,getOrDefault方法将返回默认值。然后,put方法将添加一个新的(key, value)对。因此,所有断言都会通过。

4.3 putIfAbsent方法

putIfAbsent方法的作用与前面的getOrDefault和put方法的组合相同。

如果HashMap中不存在具有提供的键的对,则putIfAbsent方法将添加该对。但是,如果有这样的对,putIfAbsent方法将不会更改Map

但是,有一个例外:如果现有对具有空值,则该对将更新为新值

让我们对putIfAbsent方法进行测试,我们将通过两个示例来测试该行为:

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

Map上有一个apple键,putIfAbsent方法不会更改其当前值。

与此同时,pear键从Map上消失了。因此,它将被添加。

4.4 compute方法

compute方法根据作为第二个参数提供的BiFunction更新键的值。如果键在Map上不存在,我们可以期待一个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存在,它在Map中的值将被更新。另一方面,没有键blueberry,因此在最后一个断言中第二次调用compute方法将导致NullPointerException。

4.5 computeIfAbsent方法

如果HashMap中没有特定键的对,则前面的方法会引发异常。computeIfAbsent方法将通过添加键值对(如果不存在)来更新Map

让我们测试一下这个方法的行为:

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

Map上不存在lemon键。因此,computeIfAbsent方法添加一个条目。

4.6 computeIfPresent方法

如果键存在于HashMap中,则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在Map中,并且computeIfPresent方法将根据BiFunction更新值。

4.7 merge方法

如果存在这样的键,则merge方法使用BiFunction更新HashMap中键的值。否则,它将添加一个新的(key,value)对,并将值设置为作为该方法的第二个参数提供的值

那么,让我们检查一下这个方法的行为:

@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上执行merge方法。它已经在Map上,因此它的值将会改变。它将是我们传递给该方法的defaultValue参数的平方。

strawberry键不在Map上。因此,merge方法将以defaultValue作为值添加它。

5. 总结

在本文中,我们介绍了几种更新与HashMap中的键关联的值的方法。

首先,我们从最常见的方法开始。然后,我们展示了自Java 8以来可用的几种方法。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章