Подтвердить что ты не робот

Преобразование и фильтрация карты Java с потоками

У меня есть карта Java, которую я хотел бы преобразовать и фильтровать. В качестве тривиального примера предположим, что я хочу преобразовать все значения в целые числа, а затем удалить нечетные записи.

Map<String, String> input = new HashMap<>();
input.put("a", "1234");
input.put("b", "2345");
input.put("c", "3456");
input.put("d", "4567");

Map<String, Integer> output = input.entrySet().stream()
        .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> Integer.parseInt(e.getValue())
        ))
        .entrySet().stream()
        .filter(e -> e.getValue() % 2 == 0)
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));


System.out.println(output.toString());

Это правильно и дает: {a=1234, c=3456}

Однако я не могу не задаться вопросом, есть ли способ избежать вызова .entrySet().stream() дважды.

Есть ли способ выполнить операции преобразования и фильтрации и вызвать .collect() только один раз в конце?

4b9b3361

Ответ 1

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

Map<String, Integer> output =
    input.entrySet()
         .stream()
         .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), Integer.valueOf(e.getValue())))
         .filter(e -> e.getValue() % 2 == 0)
         .collect(Collectors.toMap(
             Map.Entry::getKey,
             Map.Entry::getValue
         ));

Обратите внимание, что я использовал Integer.valueOf вместо parseInt, так как мы действительно хотим иметь коробку int.


Если у вас есть возможность использовать библиотеку StreamEx, вы можете сделать это довольно просто:

Map<String, Integer> output =
    EntryStream.of(input).mapValues(Integer::valueOf).filterValues(v -> v % 2 == 0).toMap();

Ответ 2

Один из способов решения проблемы с гораздо меньшими накладными расходами - переместить отображение и фильтрацию в коллекционер.

Map<String, Integer> output = input.entrySet().stream().collect(
    HashMap::new,
    (map,e)->{ int i=Integer.parseInt(e.getValue()); if(i%2==0) map.put(e.getKey(), i); },
    Map::putAll);

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

По сравнению с тем, что делает Collectors.toMap(…), операция также упрощается с помощью Map.put, а не Map.merge, как мы знаем заранее, что нам не нужно обрабатывать ключевые коллизии здесь.

Однако, если вы не хотите использовать параллельное выполнение, вы также можете рассмотреть обычный цикл

HashMap<String,Integer> output=new HashMap<>();
for(Map.Entry<String, String> e: input.entrySet()) {
    int i = Integer.parseInt(e.getValue());
    if(i%2==0) output.put(e.getKey(), i);
}

или внутренний вариант итерации:

HashMap<String,Integer> output=new HashMap<>();
input.forEach((k,v)->{ int i = Integer.parseInt(v); if(i%2==0) output.put(k, i); });

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

Ответ 3

Вы можете использовать метод Stream.collect(supplier, accumulator, combiner), чтобы преобразовать записи и условно скопировать их:

Map<String, Integer> even = input.entrySet().stream().collect(
    HashMap::new,
    (m, e) -> Optional.ofNullable(e)
            .map(Map.Entry::getValue)
            .map(Integer::valueOf)
            .filter(i -> i % 2 == 0)
            .ifPresent(i -> m.put(e.getKey(), i)),
    Map::putAll);

System.out.println(even); // {a=1234, c=3456}

Здесь, внутри аккумулятора, я использую методы Optional для применения как преобразования, так и предиката, и, если необязательное значение все еще присутствует, я добавляю его к собираемой карте.

Ответ 4

Другой способ сделать это - удалить значения, которые вы не хотите от преобразованного Map:

Map<String, Integer> output = input.entrySet().stream()
        .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> Integer.parseInt(e.getValue()),
                (a, b) -> { throw new AssertionError(); },
                HashMap::new
         ));
output.values().removeIf(v -> v % 2 != 0);

Предполагается, что в качестве результата вы захотите изменить значение Map, если вы не можете создать неизменяемое значение из output.


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

input.replaceAll((k, v) -> v + " example");
input.values().removeIf(v -> v.length() > 10);

Это также предполагает, что input является изменяемым.


Я не рекомендую это делать, потому что он не будет работать для всех допустимых реализаций Map и может перестать работать для HashMap в будущем, но в настоящее время вы можете использовать replaceAll и применить a HashMap для изменения тип значений:

((Map)input).replaceAll((k, v) -> Integer.parseInt((String)v));
Map<String, Integer> output = (Map)input;
output.values().removeIf(v -> v % 2 != 0);

Это также даст вам предупреждения о безопасности типа и если вы попытаетесь получить значение из Map через ссылку старого типа следующим образом:

String ex = input.get("a");

Он выкинет ClassCastException.


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

public static <K, VO, VN, M extends Map<K, VN>> M transformValues(
        Map<? extends K, ? extends VO> old, 
        Function<? super VO, ? extends VN> f, 
        Supplier<? extends M> mapFactory){
    return old.entrySet().stream().collect(Collectors.toMap(
            Entry::getKey, 
            e -> f.apply(e.getValue()), 
            (a, b) -> { throw new IllegalStateException("Duplicate keys for values " + a + " " + b); },
            mapFactory));
}

И используйте его следующим образом:

    Map<String, Integer> output = transformValues(input, Integer::parseInt, HashMap::new);
    output.values().removeIf(v -> v % 2 != 0);

Обратите внимание, что исключение дублирующего ключа может быть выбрано, если, например, old Map является IdentityHashMap, а mapFactory создает HashMap.

Ответ 5

Guava ваш друг:

Map<String, Integer> output = Maps.filterValues(Maps.transformValues(input, Integer::valueOf), i -> i % 2 == 0);

Имейте в виду, что output представляет собой преобразованный, отфильтрованный вид input. Вам нужно будет сделать копию, если вы хотите работать с ними независимо.

Ответ 6

Вот код AbacusUtil

Map<String, String> input = N.asMap("a", "1234", "b", "2345", "c", "3456", "d", "4567");

Map<String, Integer> output = Stream.of(input)
                          .groupBy(e -> e.getKey(), e -> N.asInt(e.getValue()))
                          .filter(e -> e.getValue() % 2 == 0)
                          .toMap(Map.Entry::getKey, Map.Entry::getValue);

N.println(output.toString());

Декларация: Я разработчик AbacusUtil.