Почему сигнатура метода Map.compute() с использованием лямбда-функции выглядит подобным образом?

Вот код из теории:

import java.util.List;
import java.util.HashMap;

public class WordCountExample {
    public static void main(String[] args) {
        var words = List.of("apple", "banana", "apple", "orange", "banana", "apple");
        var wordCount = new HashMap<String, Integer>();

        words.forEach((word) -> {
            wordCount.compute(word, (key, count) -> count == null ? 1 : count + 1);
        });

        System.out.println(wordCount); // Output: {orange=1, banana=2, apple=3}
    }
}

Почему сигнатура функции имеет подобный вид? Строка из теории:

wordCount.compute(word, (key, count) -> count == null ? 1 : count + 1);

Если мы получаем на вход текущее значение ключа и выполняем с ним необходимые операции, то для чего второй переменной мы снова даем по сути тот же самый key? В таком виде, как (word, count) разве нельзя передать в .compute()? Почему? Что-то я упускаю, но не могу понять что именно. Если даже не брать в учет лямбда запись, то для чего здесь интерфейс BiFunction? Чтобы была возможность поменять значение по данному ключу, используя сам ключ при этом, хоть в учебном примере кода это не было показано?

Не вдаваясь в детали реализации и типы, что-то вроде {orange= "orange count is 1", banana="banana count is 2", apple="apple count is 3"}, где в значении используется и имя самого ключа, и тогда использование BiFunction себя оправдывает?

И в таком случае, тем не менее, я не понимаю для чего в .compute() отдельно передавать key.

Помогите, пожалуйста, разобраться. Видимо не понимаю чего-то важного


Ответы (1 шт):

Автор решения: Nowhere Man

Метод Map<K, V>::V compute(K k, (K key, V value) -> ...) вычисляет новое значение для заданного ключа key на основании существующего значения value, которое может быть равно null, если ключа k в данной мапе пока не существует.

Если мы получаем на вход текущее значение ключа и выполняем с ним необходимые операции, то для чего второй переменной мы снова даем по сути тот же самый key? В таком виде, как (word, count) разве нельзя передать в .compute()?

В данном случае переменная word уже определена в лямбде для метода forEach, и она может передаваться только как первый параметр в метод compute, который ожидает другую лямбда-функцию со своими аргументами.
При попытке использовать то же название (word, count) -> ... возникнет ошибка компиляции error: variable word is already defined, поэтому данную лямбду следует определить со своим названием для аргумента key, уникальным в контексте данного метода: .compute(word, (key, count) -> ...).

Разумеется, вместо значения key при вызове лямбды будет подставляться значение из переменной word.

Если рассмотреть подробнее пример из вопроса, при первой попытке вычислить значение для ключа "apple":

var words = List.of("apple", "banana", "apple", "orange", "banana", "apple");
var wordCount = new HashMap<String, Integer>();

words.forEach((word) -> {
    wordCount.compute(word, (key, count) -> count == null ? 1 : count + 1);
});

лямбда будет вызвана с параметрами ("apple", null), так как ключ не найден и этот null следует обработать. При второй попытке count уже не будет null: ("apple", 1) -> 1 + 1.

В частности поэтому использование аналогичной функции Map::merge может выглядеть логичнее / понятнее: ключу соответствует некоторое значение по умолчанию, и затем вызывается функция для модификации текущего значения (V oldVal, V newVal) -> oldVal + newVal (которое всегда будет существовать в мапе).

words.forEach((word) -> {
    wordCount.merge(word, 1, Integer::sum);
});
→ Ссылка