2016-06-28 4 views
1

Я пытаюсь изучить Java 8 Stream и когда я пытаюсь преобразовать некоторую функцию в java8 для практики. Я встречаюсь с проблемой.Объединить список карт с использованием Java 8 Stream API

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

/* 
* input example: 
* [ 
    { 
     "k1": { "kk1": 1, "kk2": 2}, 
     "k2": {"kk1": 3, "kk2": 4} 
    } 
    { 
     "k1": { "kk1": 10, "kk2": 20}, 
     "k2": {"kk1": 30, "kk2": 40} 
    } 
    ] 
* output: 
* { 
     "k1": { "kk1": 11, "kk2": 22}, 
     "k2": {"kk1": 33, "kk2": 44} 
    } 
* 
* 
*/ 
private static Map<String, Map<String, Long>> mergeMapsValue(List<Map<String, Map<String, Long>>> valueList) { 
    Set<String> keys_1 = valueList.get(0).keySet(); 
    Set<String> keys_2 = valueList.get(0).entrySet().iterator().next().getValue().keySet(); 
    Map<String, Map<String, Long>> result = new HashMap<>(); 
    for (String k1: keys_1) { 
     result.put(k1, new HashMap<>()); 
     for (String k2: keys_2) { 
      long total = 0; 
      for (Map<String, Map<String, Long>> mmap: valueList) { 
       Map<String, Long> m = mmap.get(k1); 
       if (m != null && m.get(k2) != null) { 
        total += m.get(k2); 
       } 
      } 
      result.get(k1).put(k2, total); 
     } 
    } 
    return result; 
} 
+0

Итак, все карты одинаковы - то есть имеют одинаковые ключи на обоих уровнях? –

+4

Это не служба перевода кода. Вам нужно показать нам, что вы уже пробовали, чтобы мы могли сказать вам, что вы делаете неправильно. – explv

+2

Сначала вы должны переосмыслить свой первоначальный подход, то есть, что нужно перебирать во внешнем цикле и во внутреннем цикле. – Holger

ответ

5

Трюк здесь - правильно собрать внутренние карты. Рабочий будет:

  • Flat карта список карта List<Map<String, Map<String, Long>>> в поток карты записей Stream<Map.Entry<String, Map<String, Long>>>.
  • Группируйте по клавише каждой из этих записей, а для значений, сопоставленных с одним и тем же ключом, объедините две карты вместе.

Сбор карты путем их объединения, в идеале гарантировать flatMapping коллектор, который, к сожалению, не существует в Java 8, хотя it will exist in Java 9 (см JDK-8071600). Для Java 8 можно использовать тот, который предоставляется библиотекой StreamEx (и используйте MoreCollectors.flatMapping в следующем коде).

private static Map<String, Map<String, Long>> mergeMapsValue(List<Map<String, Map<String, Long>>> valueList) { 
    return valueList.stream() 
        .flatMap(e -> e.entrySet().stream()) 
        .collect(Collectors.groupingBy(
         Map.Entry::getKey, 
         Collectors.flatMapping(
          e -> e.getValue().entrySet().stream(), 
          Collectors.<Map.Entry<String,Long>,String,Long>toMap(Map.Entry::getKey, Map.Entry::getValue, Long::sum) 
         ) 
        )); 
} 

Без использования этого удобного коллектора, мы все еще можем построить наши собственные равноценную семантику:

private static Map<String, Map<String, Long>> mergeMapsValue2(List<Map<String, Map<String, Long>>> valueList) { 
    return valueList.stream() 
        .flatMap(e -> e.entrySet().stream()) 
        .collect(Collectors.groupingBy(
         Map.Entry::getKey, 
         Collector.of(
          HashMap::new, 
          (r, t) -> t.getValue().forEach((k, v) -> r.merge(k, v, Long::sum)), 
          (r1, r2) -> { r2.forEach((k, v) -> r1.merge(k, v, Long::sum)); return r1; } 
         ) 
        )); 
} 
+1

Я украл вашу 'Long :: sum' - я продолжаю забывать, что существует. Я думаю, что ваш подход «Поток» лучше всего в целом, поскольку он не дает промежуточных результатов «Список», хотя и ваши, и мои полностью неразборчивы. Я бы выступал за «foreach» с помощью метода Java 8 «Карта» ... –

+1

@BoristheSpider Да, использование старых добрых циклов, возможно, было бы в целом лучше здесь. – Tunaki

+0

Извините, я не понимаю ваш метод, поэтому я добавляю printf в функцию. Кажется, лямбда (r1, r2) -> {r2.forEach ((k, v) -> r1.merge (k, v, Long :: sum)); return r1; } не выполняются вообще. Можете ли вы объяснить, как работает Collector.of? Я не могу получить много информации об этом, выполнив поиск. – yunfan

4

В качестве отправной точки, преобразование использовать computeIfAbsent и merge дает нам следующее:

private static <K1, K2> Map<K1, Map<K2, Long>> mergeMapsValue(List<Map<K1, Map<K2, Long>>> valueList) { 
    final Map<K1, Map<K2, Long>> result = new HashMap<>(); 
    for (final Map<K1, Map<K2, Long>> map : valueList) { 
     for (final Map.Entry<K1, Map<K2, Long>> sub : map.entrySet()) { 
      for (final Map.Entry<K2, Long> subsub : sub.getValue().entrySet()) { 
       result.computeIfAbsent(sub.getKey(), k1 -> new HashMap<>()) 
         .merge(subsub.getKey(), subsub.getValue(), Long::sum); 
      } 
     } 
    } 
    return result; 
} 

Это снимает большую часть логики от вашего внутреннего цикла.


Этот код ниже wrong, я оставляю его здесь для справки.

Преобразование в API Stream API не позволит сделать его более аккуратным, но дает вам возможность двигаться вперед.

import static java.util.stream.Collectors.collectingAndThen; 
import static java.util.stream.Collectors.groupingBy; 
import static java.util.stream.Collectors.mapping; 
import static java.util.stream.Collectors.toList; 

private static <K1, K2> Map<K1, Map<K2, Long>> mergeMapsValue(List<Map<K1, Map<K2, Long>>> valueList) { 
    return valueList.stream() 
      .flatMap(v -> v.entrySet().stream()) 
      .collect(groupingBy(Entry::getKey, collectingAndThen(mapping(Entry::getValue, toList()), l -> l.stream() 
        .reduce(new HashMap<>(), (l2, r2) -> { 
         r2.forEach((k, v) -> l2.merge(k, v, Long::sum); 
         return l2; 
        })))); 
} 

Это то, что мне удалось придумать - это ужасно. Проблема в том, что с подходом foreach у вас есть ссылка на каждый уровень итерации - это делает логику простой. При функциональном подходе вам необходимо учитывать каждую операцию складывания отдельно.

Как это работает?

Мы сначала stream() наш List<Map<K1, Map<K2, Long>>>, давая Stream<Map<K1, Map<K2, Long>>>. Затем мы получаем flatMap каждый элемент, давая Stream<Entry<K1, Map<K2, Long>>> - поэтому мы сглаживаем первое измерение. Но мы не можем сгладить, так как нам нужно значение K1.

Итак, мы используем collect(groupingBy) на K1, что дает нам Map<K1, SOMETHING> - что-то?

Ну, сначала мы используем mapping(Entry::getValue, toList()), чтобы дать нам Map<K1, List<Map<K2, Long>>>. Затем мы используем collectingAndThen, чтобы принять это List<Map<K2, Long>> и уменьшить его. Обратите внимание, что это означает, что мы производим промежуточный List, который является расточительным - вы можете обойти это, используя пользовательский номер Collector.

Для этого мы используем List.stream().reduce(a, b), где a - начальное значение, а b - операция «сгиба». a установлено в new HashMap<>() и b принимает два значения: либо начальное значение, либо результат предыдущего применения функции и текущего элемента в List. Поэтому мы для каждого элемента в List используют Map.merge для объединения значений.

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

+2

НЕ используйте 'reduce', когда вы изменяете аргументы функции. – Holger

+0

@ Holger. Я не понимаю. Кажется, его код не изменяет аргументы функции. – yunfan

+1

@yunfan: функция, переданная 'reduce', изменяет карту' l2', вызывая на ней 'merge' для каждого отображения' r2'. – Holger

0

Я взял flatMap(e -> e.entrySet().stream()) части из Tunaki, но использовал более короткий вариант для коллектора:

Map<String, Integer> merged = maps.stream() 
    .flatMap(map -> map.entrySet().stream()) 
    .collect(Collectors.toMap(
    Map.Entry::getKey, Map.Entry::getValue, Integer::sum)); 

Более сложный пример:

Map<String, Integer> a = new HashMap<String, Integer>() {{ 
    put("a", 2); 
    put("b", 5); 
}}; 
Map<String, Integer> b = new HashMap<String, Integer>() {{ 
    put("a", 7); 
}}; 
List<Map<String, Integer>> maps = Arrays.asList(a, b); 

Map<String, Integer> merged = maps.stream() 
    .flatMap(map -> map.entrySet().stream()) 
    .collect(Collectors.toMap(
    Map.Entry::getKey, Map.Entry::getValue, Integer::sum)); 

assert merged.get("a") == 9; 
assert merged.get("b") == 5; 
Смежные вопросы