Как использовать новую функцию 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");

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


Другой способ сделать studentListSubjectWise.stream().collect(Collectors.GroupingBy(subj::getSubjName, Collectors.toList());мульти-карту в Java 8 (и более сжатый) - просто сделать. Это создает мульти-карту типа Map<T,List<T>в JDK, только более кратко, imho.
— Zombies
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.