Как использовать новую функцию computeIfAbsent?

115

Я очень хочу использовать Map.computeIfAbsent, но с тех пор, как лямбды не использовались в старших классах, прошло слишком много времени.

Практически прямо из документации: он дает пример старого способа делать что-то:

Map<String, Boolean> whoLetDogsOut = new ConcurrentHashMap<>();
String key = "snoop";
if (whoLetDogsOut.get(key) == null) {
  Boolean isLetOut = tryToLetOut(key);
  if (isLetOut != null)
    map.putIfAbsent(key, isLetOut);
}

И новый способ:

map.computeIfAbsent(key, k -> new Value(f(k)));

Но в их примере, я думаю, я не совсем «понимаю». Как мне преобразовать код, чтобы использовать новый лямбда-способ выражения этого?

Бенджамин Х
источник
Я не уверен, что вам не понятно из приведенного здесь примера?
Луи Вассерман,
2
Что такое «к»? Определяется ли это переменная? Как насчет «нового значения» - это что-то из java 8 или представляющий объект, который мне нужно определить или переопределить? whoLetDogsOut.computeIfAbsent (key, k -> new Boolean (tryToLetOut (k))) не компилируется, поэтому мне что-то не хватает ...
Бенджамин Х
Что именно не компилируется? Какую ошибку выдает?
axtavt
Temp.java:26: ошибка: недопустимое начало выражения whoLetDogsOut.computeIfAbsent (key, k -> new Boolean (tryToLetOut (k))); (указывая на ">")
Benjamin H
Компилирует мне нормально. Убедитесь, что вы действительно используете компилятор Java 8. Работают ли другие функции Java 8?
axtavt

Ответы:

96

Предположим, у вас есть следующий код:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Test {
    public static void main(String[] s) {
        Map<String, Boolean> whoLetDogsOut = new ConcurrentHashMap<>();
        whoLetDogsOut.computeIfAbsent("snoop", k -> f(k));
        whoLetDogsOut.computeIfAbsent("snoop", k -> f(k));
    }
    static boolean f(String s) {
        System.out.println("creating a value for \""+s+'"');
        return s.isEmpty();
    }
}

Тогда вы увидите сообщение creating a value for "snoop"ровно один раз, так как при втором вызове computeIfAbsentуже есть значение для этого ключа. В kлямбда-выражении k -> f(k)просто подставка (параметр) для ключа, который карта передаст вашей лямбда-выражению для вычисления значения. Итак, в этом примере ключ передается при вызове функции.

В качестве альтернативы вы можете написать: whoLetDogsOut.computeIfAbsent("snoop", k -> k.isEmpty());для достижения того же результата без вспомогательного метода (но тогда вы не увидите вывод отладки). И что еще проще, так как это простое делегирование существующему методу, которое вы могли бы написать: whoLetDogsOut.computeIfAbsent("snoop", String::isEmpty);Это делегирование не требует записи каких-либо параметров.

Чтобы быть ближе к примеру в вашем вопросе, вы можете написать его как whoLetDogsOut.computeIfAbsent("snoop", key -> tryToLetOut(key));(неважно, назовете ли вы параметр kили key). Или напишите его как whoLetDogsOut.computeIfAbsent("snoop", MyClass::tryToLetOut);if tryToLetOutis staticили whoLetDogsOut.computeIfAbsent("snoop", this::tryToLetOut);if tryToLetOutis a instance method.

Хольгер
источник
114

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

Мы можем начать с определения карты и помещения в нее значений для базовых случаев, а именно, fibonnaci(0)и fibonacci(1):

private static Map<Integer,Long> memo = new HashMap<>();
static {
   memo.put(0,0L); //fibonacci(0)
   memo.put(1,1L); //fibonacci(1)
}

А для индуктивного шага все, что нам нужно сделать, это переопределить нашу функцию Фибоначчи следующим образом:

public static long fibonacci(int x) {
   return memo.computeIfAbsent(x, n -> fibonacci(n-2) + fibonacci(n-1));
}

Как видите, метод computeIfAbsentбудет использовать предоставленное лямбда-выражение для вычисления числа Фибоначчи, когда число не присутствует на карте. Это представляет собой значительное улучшение по сравнению с традиционным древовидным рекурсивным алгоритмом.

Эдвин Далорцо
источник
18
Красивое однострочное преобразование в динамическое программирование. Очень красиво.
Benjamin H
3
Вы можете получить меньше рекурсивных вызовов, если сначала выполните вызов (n-2)?
Торбьёрн Равн Андерсен
10
Вам следует быть более осторожными при рекурсивном использовании computeIfAbsent. Для получения дополнительной информации, пожалуйста, посетите stackoverflow.com/questions/28840047/…
Аджит Кумар,
12
Этот код приводит HashMapк повреждению внутренних компонентов, как и в случае с bugs.openjdk.java.net/browse/JDK-8172951, и не работает с ConcurrentModificationExceptionJava 9 ( bugs.openjdk.java.net/browse/JDK-8071667 )
Петр Findeisen
23
В документации буквально говорится, что функция сопоставления не должна изменять эту карту во время вычислений , поэтому этот ответ явно неверен.
fps
41

Другой пример. При построении сложной карты карт метод computeIfAbsent () заменяет метод get () карты. Посредством объединения вызовов computeIfAbsent () недостающие контейнеры создаются на лету с помощью предоставленных лямбда-выражений:

  // Stores regional movie ratings
  Map<String, Map<Integer, Set<String>>> regionalMovieRatings = new TreeMap<>();

  // This will throw NullPointerException!
  regionalMovieRatings.get("New York").get(5).add("Boyhood");

  // This will work
  regionalMovieRatings
    .computeIfAbsent("New York", region -> new TreeMap<>())
    .computeIfAbsent(5, rating -> new TreeSet<>())
    .add("Boyhood");
hexabc
источник
31

мульти-карта

Это действительно полезно, если вы хотите создать мульти-карту, не прибегая к библиотеке Google Guava для реализацииMultiMap .

Например, предположим, что вы хотите сохранить список студентов, поступивших на определенный предмет.

Обычное решение для этого с использованием библиотеки JDK:

Map<String,List<String>> studentListSubjectWise = new TreeMap<>();
List<String>lis = studentListSubjectWise.get("a");
if(lis == null) {
    lis = new ArrayList<>();
}
lis.add("John");

//continue....

Поскольку у него есть шаблонный код, люди склонны использовать Guava Mutltimap.

Используя Map.computeIfAbsent, мы можем написать в одной строке без guava Multimap следующим образом.

studentListSubjectWise.computeIfAbsent("a", (x -> new ArrayList<>())).add("John");

Стюарт Маркс и Брайан Гетц хорошо обсудили это https://www.youtube.com/watch?v=9uTVXxJjuco

Nantitv
источник
Другой способ сделать studentListSubjectWise.stream().collect(Collectors.GroupingBy(subj::getSubjName, Collectors.toList());мульти-карту в Java 8 (и более сжатый) - просто сделать. Это создает мульти-карту типа Map<T,List<T>в JDK, только более кратко, imho.
Zombies