У меня есть 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. Я не мог придумать более чистое потоковое решение.
@leftaroundabout На разумном языке, который распознает композиции функторов как функторы, это должно быть просто fmap (*2) для комбинации map и option ...
@AndreyTyukin хм, интересный момент, но есть ли какой-нибудь язык, который делает это автоматически и все же позволяет отображать Только во внешний функтор, когда это необходимо? Кажется довольно нетривиальным перегрузить выбор функтора таким образом. Что определенно разумно для языка, так это позволить вам легко обернуть композицию двух функторов в новый пользовательский функтор.
@leftaroundabout Давайте начнем флейм; Какой из них является «Лучшим языком программирования®»? ;-) А если серьезно: Java делает имеет "функторы" (по крайней мере, лямбды довольно близки ...), и когда возникает необходимость в некоторой абстракции, ее обычно можно ввести. Фактические вызовы в мой ответ очень похожи на то, что вы предложили.
@ Marco13 да, я знаю, что мне следует сопротивляться своему желанию публиковать подобные комментарии ... - функтор на самом деле сильно отличается от лямбда; вы можете подумать о функциональные объекты (которые в C++ неправильно называются «функторами»). В любом случае, адаптер nullSafe, который вы показали в своем ответе, довольно хорош. (Фактически, он в основном реализует функтор опций, который Java неявно несет с его возможностью пустых ссылок.)
@ Marco13 Нет, пожалуйста, давайте не будем этого делать ...;) Вот в чем я и заключал: можно представить язык, в котором ту же идею можно было бы выразить вдвое короче даже по сравнению с Haskell, так что, возможно, нам не стоит считать, сколько в нем символов берет. Давайте просто ответим на вопрос, решим конкретную проблему, а в остальном воздержимся от сравнения длины кода на разных языках.




Моим первым побуждением было предложить 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)); после создания новой пустой карты; это позволяет избежать копирования / замены значений на карте
@Eran, хотя это правильно, replaceAll необходим, когда есть дополнительный фильтр или вычисления на основе Key, чего здесь нет. ИМО это может быть упрощенным
Вы можете добиться этого, преобразовав в поток что-то вроде:
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() на одной карте для смены другой карты в моей книге запрещено. Я бы предпочел вместо этого собрать поток на новую карту. Это канонический и признанный способ использования потоков.
@ PetrJaneček: хорошо, что forEach в любом случае может действовать только посредством побочных эффектов, также этот ответ нет использует .stream() где угодно, что имеет большое значение ... Но это может быть действительно упрощенный
Это можно сделать так
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 немного непривычно длинный). Он использует то, что записи поддерживаются картой; новая карта не создается.
Я не хотел критиковать, даже наоборот. Не было голосов за, и другие уже получили голоса, несмотря на то, что ваше решение было очевидным и эффективным.
@JoopEggen, а ты даже не нужно стримить
@Eugene действительно someMap.forEach(e -> if (e.getValue() != null) e.setValue.... возможен, но без filter. Так что stream+filter+forEach также кажется хорошим разделением. Есть достаточно хороших ответов, из которых можно выбирать.
Это удаляет значения null. Вопрос хочет их сохранить.
@OrangeDog - нет. Я просто отфильтровываю нули, чтобы умножить существующие значения. Нулевые значения останутся, я не создаю новую карту и не удаляю их. В случае сбора он удалит нулевые значения, но на данный момент он просто отфильтровывает нулевые значения, чтобы умножить существующие значения.
@CodeMatrix извините, да, вы изменяете существующую карту, а не собираете новую
Используйте вот так.
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);
(Сейчас. Автор отредактировал свой ответ, чтобы исправить проблему, которую увидел Эран.)
@ PetrJaneček действительно, но он не сохраняет нулевые значения, как того требует OP.
@Earn, я пропустил или неверно истолковал какую-то часть вопроса, особенно "null as null". Спасибо, что указали.
Попробуйте что-то подобное с api потока java 8
Map<String, Double> newMap = oldMap.entrySet().stream()
.collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue() == null ? null: x.getValue()*2));
как это возможно ?
Вы можете сделать это с помощью этого кода:
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
@Eugene Это тоже положительный момент. ОП сказал, что "карта, возвращаемая someMapFunction, является неизменной картой, поэтому это невозможно сделать на месте с помощью Map.replaceAll".
@ PetrJaneček Я удалил этот комментарий, единственным недостатком в данном случае является изменение размеров HashMap.
Чтобы сохранить нулевые значения, вы можете использовать что-то простое, например:
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() на одной карте для смены другой карты в моей книге запрещено. Я бы предпочел вместо этого собрать поток на новую карту. Это канонический и признанный способ использования потоков.
@ PetrJaneček Вы можете применить его к копии someMap, если предпочитаете этот стиль.
В качестве альтернативы потоковой передаче и / или копированию решений в 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 никогда не будет изменен / хеширован.
аааа смена часовых поясов, ты живешь слишком далеко на востоке :)
Если вас устраивают значения 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
Ах, как хороши нулевые указатели ... на разумном языке с функторами и типобезопасными типами опций решением этой проблемы было бы просто
fmap (fmap (*2)), готово.