Перейти к основному содержимому

Обновите значение, связанное с ключом в HashMap

· 7 мин. чтения

1. Обзор

В этом руководстве будут рассмотрены различные подходы к обновлению значения, связанного с данным ключом в HashMap . Сначала мы рассмотрим некоторые распространенные решения, использующие только те функции, которые были доступны до Java 8. Затем мы рассмотрим некоторые дополнительные решения, доступные в Java 8 и более поздних версиях.

2. Инициализация нашего примера HashMap

Чтобы показать, как обновлять значения в HashMap , мы должны сначала создать и заполнить его. Итак, мы создадим карту с фруктами в качестве ключей и их ценами в качестве значений:

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

Мы будем использовать этот HashMap в нашем примере. Теперь мы готовы познакомиться с методами обновления значения, связанного с ключом HashMap .

3. До Java 8

Начнем с методов, которые были доступны до 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"));
}

Ключевое яблоко уже на карте. Следовательно, первое утверждение пройдет.

Поскольку оранжевого цвета на карте нет, метод put его добавит. Следовательно, два других утверждения также пройдут.

3.2. Комбинация методов containsKey и put

Комбинация методов containsKey и put — это еще один способ обновить значение ключа в HashMap . Эта опция проверяет, содержит ли карта уже ключ. В таком случае мы можем обновить значение с помощью метода 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"));
}

Поскольку яблоко находится на карте, метод containsKey вернет true . Следовательно, вызов метода put будет выполнен, и значение будет обновлено.

4. Java 8 и выше

Начиная с Java 8 доступно множество новых методов, облегчающих процесс обновления значения ключа в HashMap. Итак, давайте познакомимся с ними.

4.1. Методы замены _

Начиная с версии 8, в интерфейсе карты доступны два перегруженных метода замены . Давайте посмотрим на сигнатуры методов: ``

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

Значение ключевого яблока будет обновлено до новой цены с помощью метода замены . Поэтому второе и третье утверждения пройдут.

Однако интересно первое утверждение . Что, если бы в нашем HashMap не было ключевого яблока ? Если мы попытаемся обновить значение несуществующего ключа, будет возвращено значение null . Принимая это во внимание, возникает другой вопрос: а что, если бы был ключ с нулевым значением? Мы не можем знать, действительно ли это значение, возвращенное из метода замены , было значением предоставленного ключа или мы пытались обновить значение несуществующего ключа. ** **

Итак, чтобы избежать недоразумений, мы можем использовать второй метод замены . Он принимает три аргумента:

  • ключ
  • текущее значение, связанное с ключом
  • новое значение для связи с ключом

Он обновит значение ключа до нового значения при одном условии: если второй аргумент является текущим значением, значение ключа будет обновлено до нового значения. Метод возвращает true в случае успешного обновления. В противном случае возвращается false .

Итак, давайте реализуем несколько тестов для проверки второго метода замены :

@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 с текущим значением ключа, это значение будет заменено.

С другой стороны, второй тест не вызывается с текущим значением. Таким образом, возвращается ложь .

4.2. Комбинация методов getOrDefault и put

Метод getOrDefault — идеальный выбор , если у нас нет записи для предоставленного ключа . В этом случае мы устанавливаем значение по умолчанию для несуществующего ключа. Затем запись добавляется на карту. При таком подходе мы можем легко избежать 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"));
}

Поскольку такого ключа нет, метод getOrDefault вернет значение по умолчанию. Затем метод put добавит новую пару (ключ, значение). Поэтому все утверждения пройдут.

4.3. Метод putIfAbsent _

Метод putIfAbsent делает то же самое, что и предыдущая комбинация методов getOrDefault и put .

Если в HashMap нет пары с предоставленным ключом, метод putIfAbsent добавит пару. Однако если такая пара есть, метод putIfAbsent не изменит карту.

Но есть исключение: если существующая пара имеет нулевое значение, то пара будет обновлена до нового значения.

Реализуем тест для метода 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"));
}

Ключевое яблоко присутствует на карте . Метод putIfAbsent не изменит свое текущее значение.

При этом на карте отсутствует ключевая груша . Следовательно, он будет добавлен .

4.4. Метод вычисления _

Метод вычисления обновляет значение ключа на основе функции 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)));
}

Как и ожидалось, поскольку ключевое яблоко существует, его значение на карте будет обновлено. С другой стороны, ключа blueberry нет , поэтому второй вызов метода вычисления в последнем утверждении приведет к NullPointerException .

4.5. Метод calculateIfAbsent _

Предыдущий метод создает исключение, если в HashMap нет пары для определенного ключа. Метод calculateIfAbsent обновит карту, добавив пару (ключ, значение), если она не существует .

Давайте проверим поведение этого метода:

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

Ключевого лимона на карте нет. Следовательно, метод computeIfAbsent добавляет запись. ``

4.6. Метод calculateIfPresent _

Метод calculateIfPresent обновляет значение ключа , если оно присутствует в HashMap .

Давайте посмотрим, как мы можем использовать этот метод:

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

Утверждение пройдет, поскольку ключевое яблоко находится на карте, а метод calculateIfPresent обновит значение в соответствии с BiFunction .

4.7. Метод слияния _

Метод слияния обновляет значение ключа в HashMap , используя BiFunction , если такой ключ есть. В противном случае он добавит новую пару (ключ, значение) со значением, установленным на значение , указанное в качестве второго аргумента метода. [](/lessons/b/-java-bifunction-interface)

Итак, давайте проверим поведение этого метода:

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

Тест сначала выполняет метод слияния на яблоке ключа . Он уже есть на карте, поэтому его значение изменится. Это будет квадрат параметра defaultValue , который мы передали методу.

Ключевой клубнички нет на карте. Поэтому метод слияния добавит его со значением defaultValue .

5. Вывод

В этой статье мы описали несколько способов обновления значения, связанного с ключом в HashMap .

Во-первых, мы начали с наиболее распространенных подходов. Затем мы показали несколько методов, доступных начиная с Java 8.

Как всегда, код этих примеров доступен на GitHub .