Есть ли чистый (и нулевой безопасный) способ умножения значений карты в 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. Я не мог придумать решение, которое было чище.

Ответы

Ответ 1

Мой первый инстинкт заключался в том, чтобы предложить Stream входной Map entrySet которая отображает значения в новые значения и заканчивается с помощью collectors.toMap().

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

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

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

Ответ 2

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

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

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

Ответ 3

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

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

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

Это показано здесь, с тремя разными операторами (один из них - 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 -check. И это null -safe, потому что... имя метода говорит так.

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

Редактировать:

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

Ответ 4

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

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}

Ответ 5

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

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

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

Ответ 6

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

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

Ответ 7

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

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 значения на карте.

Ответ 8

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

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));

Ответ 9

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

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 карта никогда не будет изменена/перефразирована.

Ответ 10

Если вы в порядке с 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 и поощрение пользователей к их обработке.

Ответ 11

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

  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);

Ответ 12

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

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