- Published on
- Updated on
Map.merge () - метод для управления всеми остальными
- Authors
Автор статьи - TOMASZ NURKIEWICZ, ссылка на оригинал: Map.merge() - One method to rule them all. Перевод опубликован с разрешения автора.
Я не часто объясняю один метод в JDK, но когда это делаю, речь идет о Map.merge. Вероятно, самая универсальная операция во вселенной - ключ-значение. А также довольно малоизвестный и редко используемый.
Метод merge()
может быть объяснен следующим образом: он либо помещает новое значение под заданным ключом (если отсутствует), либо обновляет существующий ключ с заданным значением (UPSERT
).
Давайте начнем с самого простого примера: подсчета уникальных вхождений слов. Код до Java 8 (читай: до 2014 года!) был довольно запутанным, а суть была потеряна в деталях реализации:
var map = new HashMap<String, Integer>();
words.forEach(word -> {
var prev = map.get(word);
if (prev == null) {
map.put(word, 1);
} else {
map.put(word, prev + 1);
}
});
Тем не менее, этот способ работает, и для данного участка кода выполняет желаемый результат:
var words = List.of('Foo', 'Bar', 'Foo', 'Buzz', 'Foo', 'Buzz', 'Fizz', 'Fizz')
//...
{
;(Bar = 1), (Fizz = 2), (Foo = 3), (Buzz = 2)
}
Хорошо, но давайте попробуем изменить его, чтобы избежать условной логики:
words.forEach(word -> {
map.putIfAbsent(word, 0);
map.put(word, map.get(word) + 1);
});
Это здорово! putIfAbsent()
- это необходимое зло, иначе код ломается при первом появлении ранее неизвестного слова. Кроме того, я считаю использование map.get(word)
внутри map.put()
немного неудобным. Давайте избавимся и от этого кода!
words.forEach(word -> {
map.putIfAbsent(word, 0);
map.computeIfPresent(word, (w, prev) -> prev + 1);
});
computeIfPresent()
вызывает указанное преобразование только в том случае, если существует ключ в вопросе (word
). В противном случае ничего не происходит.
Мы убедились, что ключ существует, установив его в значение ноль, поэтому приращение всегда работает. Можем ли мы сделать код лучше? Можем сократить дополнительную инициализацию, но я бы не советовал:
words.forEach(word ->
map.compute(word, (w, prev) -> prev != null ? prev + 1 : 1)
);
compute()
подобно computeIfPresent()
, но вызывается независимо от существования заданного ключа. Если значение для ключа не существует, аргумент prev
равен null
. Перенос простого if
в троичное выражение, спрятанное в лямбде, далеко не оптимально. Именно здесь и проявляет себя оператор merge()
.
Прежде чем я покажу вам окончательный вариант, давайте посмотрим немного упрощенную реализацию Map.merge()
по умолчанию:
default V merge(K key, V value, BiFunction<V, V, V> remappingFunction) {
V oldValue = get(key);
V newValue = (oldValue == null) ? value :
remappingFunction.apply(oldValue, value);
if (newValue == null) {
remove(key);
} else {
put(key, newValue);
}
return newValue;
}
Фрагмент кода стоит тысячи слов. merge()
работает в двух сценариях. Если данного ключа нет, он просто становится put(key, value)
. Однако, если указанный ключ уже имеет какое-то значение, наш remappingFunction
может объединить старый и новый. Эта функция поможет:
- перезаписать старое значение, просто вернув новое:
(old, new) -> new
- сохранить старое значение, просто возвращая старое:
(old, new) -> old
- объединить два, например:
(old, new) -> old + new
- удалить старое значение:
(old, new) -> null
Как видите, метод merge()
универсален. Так как же выглядит наша академическая проблема с merge()
? Это довольно-таки приятно:
words.forEach(word ->
map.merge(word, 1, (prev, one) -> prev + one)
);
Вы можете прочитать это следующим образом: под словом-ключом ставится 1, если оно отсутствует. В противном случае, добавляется 1 к существующему значению.
Я назвал один из параметров "один", потому что в нашем примере это всегда ... 1. К сожалению, функция remappingFunction
принимает два параметра, где второй - это значение, которое мы собираемся апсертировать (вставить или обновить). Технически, мы уже знаем это значение, поэтому (word, 1, prev -> prev + 1
) было бы гораздо проще переварить. Но такого API не существует.
Хорошо, но действительно ли метод merge()
полезен? Представьте, что у вас есть операция счета (конструктор, геттеры и другие полезные свойства опущены):
class Operation {
private final String accNo;
private final BigDecimal amount;
}
И множество операций для разных аккаунтов:
var operations = List.of(
new Operation('123', new BigDecimal('10')),
new Operation('456', new BigDecimal('1200')),
new Operation('123', new BigDecimal('-4')),
new Operation('123', new BigDecimal('8')),
new Operation('456', new BigDecimal('800')),
new Operation('456', new BigDecimal('-1500')),
new Operation('123', new BigDecimal('2')),
new Operation('123', new BigDecimal('-6.5')),
new Operation('456', new BigDecimal('-600'))
)
Мы хотели бы рассчитать баланс (общее количество операций) для каждого аккаунта. Без merge()
это сделать затруднительно:
var balances = new HashMap<String, BigDecimal>();
operations.forEach(op -> {
var key = op.getAccNo();
balances.putIfAbsent(key, BigDecimal.ZERO);
balances.computeIfPresent(key, (accNo, prev) -> prev.add(op.getAmount()));
});
Но с небольшой помощью merge()
:
balances.merge(op.getAccNo(), op.getAmount(),
(soFar, amount) -> soFar.add(amount))
);
Вы видите здесь ссылку на метод?
operations.forEach(op ->
balances.merge(op.getAccNo(), op.getAmount(), BigDecimal::add)
);
Это поразительно читабельно. Для каждой операции add
задана сумма к заданному значению accNo
. Результаты будут такими, как ожидалось:
{
;(123 = 9.5), (456 = -100)
}
ConcurrentHashMap
Map.merge()
сияет еще ярче, когда понимаешь, что метод правильно реализован в ConcurrentHashMap
. Это означает, что мы можем атомарно выполнять операции вставки и обновления. Однострочно и потокобезопасно. ConcurrentHashMap
, безусловно, безопасен для потоков, но не для многих операций, например, get()
или put()
. Однако merge()
гарантирует, что обновления не будут потеряны.
Result: 0, total votes: 0
I'm Vladimir, your guide in the expansive world of technology journalism, with a special focus on GPS technologies and mapping. My journey in this field extends over twenty fruitful years, fueled by a profound passion for technology and an insatiable curiosity to explore its frontiers.