Сопоставление карт с одной картой путем объединения ключей

#java #java-stream

Вопрос:

У меня есть структура, подобная этой:

 Map<KeyType1, Map<KeyType2, List<ValueType>>>
 

А также класс, содержащий как KeyType1, так и KeyType2, назовем его AggregatedKey. Его можно создать с помощью конструктора:

 public AggregatedKey(KeyType1 keyType1, KeyType2 keyType2)
 

Моя цель-сопоставить приведенную выше структуру с чем-то вроде:

 Map<AggregatedKey, List<ValueType>>
 

Таким образом, в основном ключи должны быть сопоставлены с одним агрегированным ключом.

Как я могу добиться этого с помощью Java 9?

Комментарии:

1. Есть ли у вас функция, которая строит агрегированный ключ из заданных двух ключей?

2. Они могут быть объединены с помощью конструктора: AggregatedKey(KeyType1, KeyType2)

Ответ №1:

Это сделает свое дело

 Map<KeyType1, Map<KeyType2, List<String>>> m = new HashMap<>();
Map<AggregatedKey, List<String>> result = new HashMap<>();
m.entrySet().forEach(entry -> {
    entry.getValue().entrySet().forEach(nestedEntry -> {
        result.put(new AggregatedKey(entry.getKey(), nestedEntry.getKey()), nestedEntry.getValue());
    });
});
 

Не забудьте внедрить hashcode/equals в свой AggregatedKey , иначе у вас возникнут некоторые проблемы с использованием result карты.

Комментарии:

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

2. @Anakin001 На самом деле, это немного другая версия for...each . В случае, если вы используете поток, вы должны получить результат вместо обновления созданной коллекции.

3. Вы правы! Хотя все и так прекрасно, все ясно, и это работает (я только что проверил это).

Ответ №2:

Вы можете сделать это так, используя потоки.

  • первый поток набора записей внешней карты
  • затем вызовите flatMap для потоковой передачи внутреннюю карту entrySet
  • создайте AggregatedKey экземпляр с помощью outerEntry.getKey() и innerEntry.getKey() обратите внимание, что для этого требуется, чтобы у этого класса был конструктор, принимающий ключи.
  • затем поместите этот экземпляр и значение из внутренней карты ( List<ValueType> ) в AbstractMap.SimpleEntry экземпляр для передачи сборщику.
  • создайте новую карту с ключом и значением SimpleEntry

Дана следующая исходная карта.

 Map<KeyType1, Map<KeyType2, List<ValueType>>> map =
        new HashMap<>();  // contains the info to be remapped.
 

Вот результат

 Map<AggregatedKey, List<ValueType>> result = map.entrySet()
        .stream()
        .flatMap(outerEntry-> outerEntry
                        .getValue().entrySet().stream()
                        .map(innerEntry -> new AbstractMap.SimpleEntry<>(
                new AggregatedKey(outerEntry.getKey(),innerEntry.getKey()),
                                innerEntry.getValue())))
        
        .collect(Collectors.toMap(
                AbstractMap.SimpleEntry::getKey,
                AbstractMap.SimpleEntry::getValue));

    }
 

Комментарии:

1. Это самый «красивый» ответ, так как он просто применяет потоковые операции и получает результат, жаль, что Stackoverflow позволяет мне выбрать только один принятый ответ 🙁

Ответ №3:

Это один из способов:

 public static void main(String[] args) {
    Map<String, Map<String, String>> map = new HashMap<>();
    Map<String, String> innerMap1 = new HashMap<>();
    Map<String, String> innerMap2 = new HashMap<>();
    innerMap1.put("k11", "v11");
    innerMap1.put("k12", "v12");
    innerMap1.put("k13", "v13");
    innerMap2.put("k21", "v22");
    innerMap2.put("k22", "v22");
    map.put("k1", innerMap1);
    map.put("k2", innerMap2);
    Map<String, String> result = map
            .entrySet()
            .stream()
            .flatMap(stringMapEntry ->
                stringMapEntry
                        .getValue()
                        .entrySet()
                        .stream()
                            .map(stringStringEntry ->
                                    new AbstractMap.SimpleEntry<String, String>(
                                            buildAggregatedKey(
                                                    stringMapEntry.getKey(), 
                                                    stringStringEntry.getKey()
                                            ), 
                                            stringStringEntry.getValue()
                                    )
                            )
            ).collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));
    System.out.println(result);
}

private static String buildAggregatedKey(String key1, String key2){
   return  key1   "_"   key2;
}
 

Где вы изменяете это buildAggregatedKey в соответствии с вашей логикой агрегирования.

Ответ №4:

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

 package prove.aggregatemap;

import org.junit.Assert;
import org.junit.Test;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class TestAggregator {

@Test
public void aggregate() {
    Map<String, List<String>> letter_map= Map.of("first", List.of("one","two","three"),
            "second", List.of("four","five","six"),
            "third", List.of("seven","eight","nine"));
    Map<String, List<String>> num_map= Map.of("first_num", List.of("1","2","3"), "second_num", List.of("4","5","6"), "third_num", List.of("7","8","9"));
    Map<String,Map<String,List<String>>> mapOfMaps=Map.of("letter",letter_map,"num",num_map);

    Map<AggregateKey, List<String>> result=mapOfMaps.entrySet().stream().flatMap(entry ->
       entry.getValue().entrySet().stream().collect(Collectors.toMap(
                inner_entry -> new AggregateKey(entry.getKey(), inner_entry.getKey()),
                inner_entry -> inner_entry.getValue())).entrySet().stream()
    ).collect(Collectors.toMap(entry->entry.getKey(),entry->entry.getValue()));
    Assert.assertEquals(List.of("one","two","three"),result.get(new AggregateKey("letter","first")));
    Assert.assertEquals(List.of("four","five","six"),result.get(new AggregateKey("letter","second")));
    Assert.assertEquals(List.of("seven","eight","nine"),result.get(new AggregateKey("letter","third")));
}
 

}

Ответ №5:

 public static final class AggregatedKey<K1, K2> {

    private final K1 one;
    private final K2 two;

    public AggregatedKey(K1 one, K2 two) {
        this.one = one;
        this.two = two;
    }
}

public static <K1, K2, V> Map<AggregatedKey<K1, K2>, List<V>> convert1(Map<K1, Map<K2, List<V>>> map) {
    Map<AggregatedKey<K1, K2>, List<V>> res = new HashMap<>();

    for (Map.Entry<K1, Map<K2, List<V>>> one : map.entrySet())
        for (Map.Entry<K2, List<V>> two : one.getValue().entrySet())
            res.put(new AggregatedKey<>(one.getKey(), two.getKey()), two.getValue());

    return res;
}

public static <K1, K2, V> Map<AggregatedKey<K1, K2>, List<V>> convert2(Map<K1, Map<K2, List<V>>> map) {
    return map.entrySet().stream()
            .flatMap(e1 -> e1.getValue().entrySet().stream()
                    .map(e2 -> new AggregatedKey<>(new AggregatedKey<>(e1.getKey(), e2.getKey()), e2.getValue())))
            .collect(Collectors.toMap(tuple -> tuple.one, tuple -> tuple.two));
}
 

Комментарии:

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

2. @Anakin001 смотрите мой обновленный ответ. Но я думаю, что converter1() это проще.

3. @Anakin001, но я отметил ответ Пилпо как принятый, так как в этом случае я хотел получить решение для потоков. Тогда почему вы приняли другой ответ? Здесь не было никаких потоков.

4. @WJS действительно, это была ошибка, я хотел сказать «функциональное программирование», и в итоге я сказал «потоки» (технически, это была ошибка чтения в первую очередь, я не видел, чтобы решение вызывало «forEach» без потоковой передачи набора записей в первую очередь, но, тем не менее, я действительно хотел иметь более «функциональное» решение).