Есть ли чистый (и нулевой безопасный) способ умножения значений карты в Java?

У меня есть Map<String, Double>, и я хочу умножить все значения на карте, скажем, на 2, но оставьте нули как нули.

Я, очевидно, могу использовать для этого цикл for, но мне было интересно, есть ли более чистый способ сделать это?

Map<String, Double> someMap = someMapFunction();
Map<String, Double> adjustedMap = new Hashmap<>();
if (someMap != null) {
    for (Map.Entry<String,Double> pair : someMap.entryset()) {
        if (pair.getValue() == null) {
            adjustedMap.put(pair.getKey(), pair.getValue());
        } else {
            adjustedMap.put(pair.getKey(), pair.getValue()*2)
        }

    }
}

Также иногда карта, возвращаемая someMapFunction, является неизменяемой картой, поэтому это невозможно сделать на месте с помощью Map.replaceAll. Я не мог придумать более чистое потоковое решение.

Ах, как хороши нулевые указатели ... на разумном языке с функторами и типобезопасными типами опций решением этой проблемы было бы просто fmap (fmap (*2)), готово.

leftaroundabout 17.10.2018 13:10

@leftaroundabout На разумном языке, который распознает композиции функторов как функторы, это должно быть просто fmap (*2) для комбинации map и option ...

Andrey Tyukin 17.10.2018 14:05

@AndreyTyukin хм, интересный момент, но есть ли какой-нибудь язык, который делает это автоматически и все же позволяет отображать Только во внешний функтор, когда это необходимо? Кажется довольно нетривиальным перегрузить выбор функтора таким образом. Что определенно разумно для языка, так это позволить вам легко обернуть композицию двух функторов в новый пользовательский функтор.

leftaroundabout 17.10.2018 14:12

@leftaroundabout Давайте начнем флейм; Какой из них является «Лучшим языком программирования®»? ;-) А если серьезно: Java делает имеет "функторы" (по крайней мере, лямбды довольно близки ...), и когда возникает необходимость в некоторой абстракции, ее обычно можно ввести. Фактические вызовы в мой ответ очень похожи на то, что вы предложили.

Marco13 17.10.2018 21:03

@ Marco13 да, я знаю, что мне следует сопротивляться своему желанию публиковать подобные комментарии ... - функтор на самом деле сильно отличается от лямбда; вы можете подумать о функциональные объекты (которые в C++ неправильно называются «функторами»). В любом случае, адаптер nullSafe, который вы показали в своем ответе, довольно хорош. (Фактически, он в основном реализует функтор опций, который Java неявно несет с его возможностью пустых ссылок.)

leftaroundabout 17.10.2018 21:16

@ Marco13 Нет, пожалуйста, давайте не будем этого делать ...;) Вот в чем я и заключал: можно представить язык, в котором ту же идею можно было бы выразить вдвое короче даже по сравнению с Haskell, так что, возможно, нам не стоит считать, сколько в нем символов берет. Давайте просто ответим на вопрос, решим конкретную проблему, а в остальном воздержимся от сравнения длины кода на разных языках.

Andrey Tyukin 17.10.2018 22:42
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
48
6
6 529
12
Перейти к ответу Данный вопрос помечен как решенный

Ответы 12

Ответ принят как подходящий

Моим первым побуждением было предложить Stream входного MapentrySet, который сопоставляет значения новым значениям и завершается collectors.toMap().

К сожалению, Collectors.toMap выдает NullPointerException, когда функция отображения значений возвращает null. Поэтому он не работает со значениями null вашего входного Map.

В качестве альтернативы, поскольку вы не можете изменить свой входной Map, я предлагаю вам создать его копию, а затем вызвать replaceAll:

Map<String, Double> adjustedMap = new HashMap<>(someMap);
adjustedMap.replaceAll ((k,v) -> v != null ? 2*v : null);

аналогичным решением может быть someMap.forEach((k, v) -> adjustedMap.put(k, v == null ? null : 2*v)); после создания новой пустой карты; это позволяет избежать копирования / замены значений на карте

fantaghirocco came to Rome 17.10.2018 14:09

@Eran, хотя это правильно, replaceAll необходим, когда есть дополнительный фильтр или вычисления на основе Key, чего здесь нет. ИМО это может быть упрощенным

Eugene 17.10.2018 16:56

Вы можете добиться этого, преобразовав в поток что-то вроде:

someMap.entrySet()
        .forEach(entry -> {
            if (entry.getValue() != null) {
                adjustedMap.put(entry.getKey(), someMap.get(entry.getKey()) * 2);
            } else {
                adjustedMap.put(entry.getKey(), null);
            }
        });

который можно сократить до:

someMap.forEach((key, value) -> {
    if (value != null) {
        adjustedMap.put(key, value * 2);
    } else {
        adjustedMap.put(key, null);
    }
});

Итак, если у вас есть карта с:

Map<String, Double> someMap = new HashMap<>();
someMap.put("test1", 1d);
someMap.put("test2", 2d);
someMap.put("test3", 3d);
someMap.put("testNull", null);
someMap.put("test4", 4d);

Вы получите такой результат:

{test4=8.0, test2=4.0, test3=6.0, testNull=null, test1=2.0}

Вместо этого ваш someMap.get(key) может быть просто value. Во всяком случае, голос против стиля. Возможно, это мое личное субъективное предпочтение, но я твердо верю, что функциональный код не должен иметь побочных эффектов. Поэтому использование forEach() на одной карте для смены другой карты в моей книге запрещено. Я бы предпочел вместо этого собрать поток на новую карту. Это канонический и признанный способ использования потоков.

Petr Janeček 17.10.2018 10:49

@ PetrJaneček: хорошо, что forEach в любом случае может действовать только посредством побочных эффектов, также этот ответ нет использует .stream() где угодно, что имеет большое значение ... Но это может быть действительно упрощенный

Eugene 17.10.2018 16:35

Это можно сделать так

someMap.entrySet().stream()
            .filter(stringDoubleEntry -> stringDoubleEntry.getValue() != null) //filter null values out
            .forEach(stringDoubleEntry -> stringDoubleEntry.setValue(stringDoubleEntry.getValue() * 2)); //multiply values which are not null

Если вам нужна вторая карта, где только значения, не равные нулю, просто используйте forEach, чтобы поместить их в новую карту.

Это решение делает то, что код OP делает в красивой форме (хотя stringDoubleEntry немного непривычно длинный). Он использует то, что записи поддерживаются картой; новая карта не создается.

Joop Eggen 17.10.2018 10:34

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

Joop Eggen 17.10.2018 10:39

@JoopEggen, а ты даже не нужно стримить

Eugene 17.10.2018 16:33

@Eugene действительно someMap.forEach(e -> if (e.getValue() != null) e.setValue.... возможен, но без filter. Так что stream+filter+forEach также кажется хорошим разделением. Есть достаточно хороших ответов, из которых можно выбирать.

Joop Eggen 17.10.2018 16:40

Это удаляет значения null. Вопрос хочет их сохранить.

OrangeDog 18.10.2018 12:52

@OrangeDog - нет. Я просто отфильтровываю нули, чтобы умножить существующие значения. Нулевые значения останутся, я не создаю новую карту и не удаляю их. В случае сбора он удалит нулевые значения, но на данный момент он просто отфильтровывает нулевые значения, чтобы умножить существующие значения.

CodeMatrix 19.10.2018 10:12

@CodeMatrix извините, да, вы изменяете существующую карту, а не собираете новую

OrangeDog 19.10.2018 10:27

Используйте вот так.

  Map<String, Double> adjustedMap = map.entrySet().stream().filter(x -> x.getValue() != null)
            .collect(Collectors.toMap(x -> x.getKey(), x -> 2*x.getValue()));
  //printing 
  adjustedMap.entrySet().stream().forEach(System.out::println);

(Сейчас. Автор отредактировал свой ответ, чтобы исправить проблему, которую увидел Эран.)

Petr Janeček 17.10.2018 10:53

@ PetrJaneček действительно, но он не сохраняет нулевые значения, как того требует OP.

c0der 17.10.2018 10:55

@Earn, я пропустил или неверно истолковал какую-то часть вопроса, особенно "null as null". Спасибо, что указали.

Pandey Amit 17.10.2018 17:20

Попробуйте что-то подобное с api потока java 8

Map<String, Double> newMap = oldMap.entrySet().stream()
    .collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue() == null ? null: x.getValue()*2));

как это возможно ?

Oğuzhan Aygün 17.10.2018 10:30

Вы можете сделать это с помощью этого кода:

Map<String, Double> map = new HashMap<>();
map.put("1", 3.0);
map.put("3", null);
map.put("2", 5.0);

Map<String, Double> res = 
map.entrySet()
   .stream()
   .collect(
           HashMap::new, 
           (m,v)->m.put(v.getKey(), v.getValue() != null ? v.getValue() * 2 : null),
           HashMap::putAll
           );

System.out.println(res);

и вывод будет:

{1=6.0, 2=10.0, 3=null}

Это позволит вам сохранить значения null на карте.

Ага. Вот и все. +1

c0der 17.10.2018 10:58

@Eugene Это тоже положительный момент. ОП сказал, что "карта, возвращаемая someMapFunction, является неизменной картой, поэтому это невозможно сделать на месте с помощью Map.replaceAll".

Petr Janeček 17.10.2018 16:34

@ PetrJaneček Я удалил этот комментарий, единственным недостатком в данном случае является изменение размеров HashMap.

Eugene 17.10.2018 16:34

Чтобы сохранить нулевые значения, вы можете использовать что-то простое, например:

someMap.keySet()
        .stream()
        .forEach(key -> adjustedMap.put(key, (someMap.get(key)) == null ? null : someMap.get(key) * 2));

Редактировать в ответ на Петр Янечек комментарий: вы можете применить предложенное к копии someMap:

adjustedMap.putAll(someMap);
adjustedMap.keySet()
       .stream()
       .forEach(key -> adjustedMap.put(key, (adjustedMap.get(key)) == null ? null : adjustedMap.get(key) * 2));

Вы должны полностью транслировать на entrySet(), вы бы избегали вызова someMap.get(key) в цикле и вместо этого могли бы просто выполнять entry.value(). Во всяком случае, голос против стиля. Возможно, это мое личное субъективное предпочтение, но я твердо верю, что функциональный код не должен иметь побочных эффектов. Поэтому использование forEach() на одной карте для смены другой карты в моей книге запрещено. Я бы предпочел вместо этого собрать поток на новую карту. Это канонический и признанный способ использования потоков.

Petr Janeček 17.10.2018 10:47

@ PetrJaneček Вы можете применить его к копии someMap, если предпочитаете этот стиль.

c0der 17.10.2018 10:53

В качестве альтернативы потоковой передаче и / или копированию решений в Google Guava существует служебный метод Maps.transformValues():

Map<String, Double> adjustedMap = Maps.transformValues(someMap, value -> (value != null) ? (2 * value) : null);

Это возвращает ленивое представление исходной карты, которое не выполняет никакой работы самостоятельно, но применяет данную функцию при необходимости. Это может быть как профессионал (если вам вряд ли когда-либо понадобятся все значения, это сэкономит вам время на вычисления), так и недостаток (если вам понадобится одно и то же значение много раз, или если вам нужно дополнительно изменить someMap. без adjustedMap, видящего изменения) в зависимости от вашего использования.

Как насчет этого?

Map<String, Double> adjustedMap = new HashMap<>(someMap);
adjustedMap.entrySet().forEach(x -> {
      if (x.getValue() != null) {
            x.setValue(x.getValue() * 2);
      }
});

Еще один способ:

Map<String, Double> someMap = someMapFunction();

int capacity = (int) (someMap.size() * 4.0 / 3.0 + 1);
Map<String, Double> adjustedMap = new HashMap<>(capacity);

if (someMap != null) someMap.forEach((k, v) -> adjustedMap.put(k, v == null ? v : v * 2));

Обратите внимание, что я создаю новую карту с коэффициентом загрузки по умолчанию (0.75 = 3.0 / 4.0) и начальной емкостью, которая всегда больше, чем size * load_factor. Это гарантирует, что размер adjustedMap никогда не будет изменен / хеширован.

на всякий случай
Eugene 17.10.2018 16:49

аааа смена часовых поясов, ты живешь слишком далеко на востоке :)

fps 17.10.2018 16:59

Если вас устраивают значения Optional, вам могут подойти следующие варианты:

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

import static java.util.stream.Collectors.toMap;


public static Map<String, Optional<Double>> g(Map<String, Double> map, Function<Double, Double> f) {
    return map.entrySet().stream().collect(
            toMap(
                    e -> e.getKey(),
                    e -> e.getValue() == null ? Optional.empty() : Optional.of(f.apply(e.getValue()))
            ));
}

а потом:

public static void main(String[] args) throws Exception {
    Map<String, Double> map = new HashMap<>();
    map.put("a", 2.0);
    map.put("b", null);
    map.put("c", 3.0);


    System.out.println(g(map, x -> x * 2));
    System.out.println(g(map, x -> Math.sin(x)));
}

печатает:

{a=Optional[4.0], b=Optional.empty, c=Optional[6.0]}
{a=Optional[0.9092974268256817], b=Optional.empty, c=Optional[0.1411200080598672]}

Это довольно чисто с созданием новой карты, делегированной Collectors, и дополнительным преимуществом возвращаемого типа Map<String, Optional<Double>>, четко указывающим на возможность null и побуждающим пользователей обращаться с ними.

Ответов уже много. Некоторые из них мне кажутся несколько сомнительными. В любом случае, большинство из них встраивают проверку null в той или иной форме.

Подход, который делает один шаг вверх по лестнице абстракции, заключается в следующем:

Вы хотите применить унарный оператор к значениям карты. Таким образом, вы можете реализовать метод, который применяет унарный оператор к значениям карты. (Все идет нормально). Теперь вам нужен «специальный» унарный оператор, безопасный для null. Затем вы можете обернуть безопасный для null унарный оператор вокруг исходного.

Это показано здесь с тремя разными операторами (один из них, если на то пошло, Math::sin):

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.UnaryOperator;

public class MapValueOps
{
    public static void main(String[] args)
    {
        Map<String, Double> map = new LinkedHashMap<String, Double>();
        map.put("A", 1.2);
        map.put("B", 2.3);
        map.put("C", null);
        map.put("D", 4.5);

        Map<String, Double> resultA = apply(map, nullSafe(d -> d * 2));
        System.out.println(resultA);

        Map<String, Double> resultB = apply(map, nullSafe(d -> d + 2));
        System.out.println(resultB);

        Map<String, Double> resultC = apply(map, nullSafe(Math::sin));
        System.out.println(resultC);

    }

    private static <T> UnaryOperator<T> nullSafe(UnaryOperator<T> op)
    {
        return t -> (t == null ? t : op.apply(t));
    }

    private static <K> Map<K, Double> apply(
        Map<K, Double> map, UnaryOperator<Double> op)
    {
        Map<K, Double> result = new LinkedHashMap<K, Double>();
        for (Entry<K, Double> entry : map.entrySet())
        {
            result.put(entry.getKey(), op.apply(entry.getValue()));
        }
        return result;
    }
}

Я думаю, что это чисто, потому что это хорошо разделяет проблемы применения оператора и выполнения проверки null. И он безопасен для null, потому что ... об этом сказано в названии метода.

(Можно было бы поспорить о том, чтобы вытащить вызов, чтобы обернуть оператор в nullSafe, один в метод apply, но дело не в этом)

Edit:

В зависимости от предполагаемого шаблона приложения можно сделать нечто подобное и применить преобразование на месте без создания новой карты, вызвав Map#replaceAll

Другие вопросы по теме