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 .