Подробно объясните Map.merge()

Java

Представляя сегодня метод слияния Map, давайте посмотрим на его мощь.

В JDK API такой метод очень особенный, он очень новый, стоит нашего времени, чтобы его понять, а также рекомендуется применить его к фактическому коду проекта, что должно вам очень помочь.Карта.слияние(). Это, вероятно, самая общая операция в Map. Но это также довольно неясно, и очень немногие используют его.

Введение

merge()Это можно объяснить следующим образом: он присваивает ключу новое значение (если его не существует) или обновляет существующий ключ заданным значением (UPSERT). Начнем с самого простого примера: подсчета уникальных вхождений слова. До java8 код был очень запутанным, и фактическая реализация фактически потеряла свое существенное значение для дизайна.

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}

Улучшение V1.

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

words.forEach(word -> {
    map.putIfAbsent(word, 0);
    map.put(word, map.get(word) + 1);
});

Такие улучшения могут удовлетворить наши требования к рефакторингу. Конкретное использование putIfAbsent() не будет подробно описано.putIfAbsentЭта строка кода определенно необходима, иначе логика сообщит об ошибке. И в приведенном ниже коде он появляется сноваput,getЭто будет странно, давайте перейдем к улучшению дизайна.

Улучшить V2

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 в троичное выражение, скрытое в лямбде, также далеко не оптимально. Прежде чем я покажу вам окончательную версию, давайте взглянем на слегка упрощенную реализацию по умолчанию.Map.merge()Анализ исходного кода.

Улучшить V3

Слияние () исходный код

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()Шерстяная ткань? код показывает, как показано ниже:


words.forEach(word ->
        map.merge(word, 1, (prev, one) -> prev + one)
);

Вы можете понять это следующим образом: если нет ключа, то инициализированное значение равно 1; в противном случае 1 добавляется к существующему значению. в кодеoneЭто постоянная, потому что в нашей сцене по умолчанию всегда плюс 1, а конкретные изменения могут быть переключены по желанию.

Сцены

Давайте представим,merge()Это действительно так полезно? Какой может быть его сцена?

например. У вас есть класс операций со счетом

class Operation {
    private final String accNo;
    private final BigDecimal amount;
}

А также серия операций на разную учетную запись:


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(), становится очень хлопотно:


Map 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код после

operations.forEach(op ->
        balances.merge(op.getAccNo(), op.getAmount(), 
                (soFar, amount) -> soFar.add(amount))
);


Затем оптимизируйте логику.

operations.forEach(op ->
        balances.merge(op.getAccNo(), op.getAmount(), BigDecimal::add)
);

Конечно результат правильный, такой лаконичный код возбуждает? Для каждой операцииaddв данномamountданныйaccNo.

{ 123 = 9.5,456 = - 100 }

ConcurrentHashMap

когда мы распространяемся наConcurrentHashMapприходи, когдаMap.mergeвнешний вид, иConcurrentHashMapСочетание очень идеальное. Такие сценарии совместного размещения предназначены для однопоточно-безопасной логики, которая автоматизирует операции вставки или обновления.

关注油腻的Java