Java 8 конвертирует список в карту поиска

У меня есть список станций, в каждой станции есть список радиостанций. Мне нужно создать карту поиска радио на станцию. Я знаю, как использовать поток Java 8 forEach для этого:

stationList.stream().forEach(station -> {
    Iterator<Long> it = station.getRadioList().iterator();
    while (it.hasNext()) {
        radioToStationMap.put(it.next(), station);
    }
});

Но я считаю, что должен быть более лаконичный способ, например, использование Collectors.mapping().

Кто-нибудь может помочь?


person Nico    schedule 06.04.2017    source источник
comment
Гарантируется ли отсутствие радиокарт более чем на одну станцию?   -  person slim    schedule 06.04.2017
comment
stationList.stream().forEach(station -> station.getRadioList().stream().forEach(rl -> radioToStationMap.put(rl, station))) недостаточно хорош для вас?   -  person ZhenyaM    schedule 06.04.2017
comment
@ZhenyaM Лучше использовать потоковые операции и collect, чем использовать forEach для записи в изменяемый список.   -  person RealSkeptic    schedule 06.04.2017
comment
Возможный дубликат Convert Список списка объектов на карту — использование лямбда-выражений в Java 8   -  person Mohamed Gad-Elrab    schedule 06.04.2017
comment
Я почти уверен, что самый «краткий способ» — вообще не использовать лямбда-выражения или потоки.   -  person VGR    schedule 06.04.2017
comment
@EricWilson, какая часть очень запутана? Я понял ответ за одно чтение. Я согласен, что это может потребовать немного больше деталей, но в остальном - это хорошо.   -  person Eugene    schedule 27.10.2019


Ответы (9)


Это должно работать, и вам не нужны третьи лица.

stationList.stream()
    .map(s -> s.getRadioList().stream().collect(Collectors.toMap(b -> b, b -> s)))
    .flatMap(map -> map.entrySet().stream())
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
person utkusonmez    schedule 06.04.2017

На основании вопроса, учитывая, что объекты Radio и Station должны быть определены как:

@lombok.Getter
class Radio {
    ...attributes with corresponding 'equals' and 'hashcode'
}

@lombok.Getter
class Station {
    List<Radio> radios;
    ... other attributes
}

Можно создать карту поиска из List<Station> в качестве входных данных с помощью такой утилиты, как:

private Map<Radio, Station> createRadioToStationMap(final List<Station> stations) {
    return stations.stream()
            // create entries with each radio and station
            .flatMap(station -> station.getRadios().stream()
                    .map(radio -> new AbstractMap.SimpleEntry<>(radio, station)))
            // collect these entries to a Map assuming unique keys
            .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey,
                    AbstractMap.SimpleEntry::getValue));
}

Немного отличается от этого поведения, если для одного и того же (равного) элемента Radio для нескольких Station нужно сгруппировать все такие станции, это может быть достигнуто с использованием groupingBy вместо toMap, например:

public Map<Radio, List<Station>> createRadioToStationGrouping(final List<Station> stations) {
    return stations.stream()
            .flatMap(station -> station.getRadios().stream()
                    .map(radio -> new AbstractMap.SimpleEntry<>(radio, station)))
            // grouping the stations of which each radio is a part of
            .collect(Collectors.groupingBy(AbstractMap.SimpleEntry::getKey,
                    Collectors.mapping(AbstractMap.SimpleEntry::getValue, Collectors.toList())));
}
person Naman    schedule 23.10.2019

Если вы готовы использовать стороннюю библиотеку, есть метод groupByEach из Eclipse Collections. :

Multimap<Radio, Station> multimap = 
    Iterate.groupByEach(stationList, Station::getRadioList);

Это также можно написать с использованием Java 8 Streams с помощью утилиты Collectors2 из коллекций Eclipse:

Multimap<Radio, Station> multimap =
        stationList.stream().collect(
                Collectors2.groupByEach(
                        Station::getRadioList,
                        Multimaps.mutable.list::empty));

Примечание. Я коммиттер Eclipse Collections.

person Donald Raab    schedule 30.10.2019

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

    stationList.stream().forEach(station -> {
        for ( Long radio : station.getRadioList() ) {
            radioToStationMap.put(radio, station);
        }
    });

or

    stationList.forEach(station -> {
        station.getRadioList().forEach(radio -> {
            radioToStationMap.put(radio, station);
        });
    });

(вы можете вызывать .forEach непосредственно в коллекциях, не нужно проходить .stream())

Самое короткое полностью «функциональное» решение, которое я смог придумать, было бы чем-то вроде

 stationList.stream().flatMap(
     station -> station.getRadioList().stream().map(radio -> new Pair<>(radio, station)))
 .collect(Collectors.toMap(p -> p.getKey(), p -> p.getValue()));

используя любой из классов Pair, доступных в сторонних библиотеках. Java 8 очень многословен для простых операций по сравнению с такими диалектами, как Xtend или Groovy.

person Artur Biesiadowski    schedule 06.04.2017
comment
Если у вас есть дубликаты в radio, вы получите java.lang.IllegalStateException: Duplicate key. Чтобы предотвратить это, вы можете добавить слияние, например Collectors.toMap(Pair::getKey, Pair::getValue, (station, station2) -> station2), в свое решение. - person Vlad Bochenin; 06.04.2017
comment
Если есть дубликаты, вероятно, следует использовать Multimap. - person Artur Biesiadowski; 06.04.2017

Как насчет:

radioToStationMap = StreamEx.of(stationList)
        .flatMapToEntry(s -> StreamEx.of(s.getRadioList()).mapToEntry(r -> s).toMap())
        .toMap();

Автор StreamEx

person user_3380739    schedule 07.04.2017

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

В C# у вас есть практические анонимные классы, которые можно использовать для этого, но в Java вам нужно будет определить хотя бы интерфейс класса Pair.

interface Radio{ }
interface Station {
    List<Radio> getRadioList();
}
interface RadioStation{
    Station station();
    Radio radio();
}

List<Station> stations = List.of();

Map<Radio,List<Station>> result= stations
   .stream()
   .flatMap( s-> s.getRadioList().stream().map( r->new RadioStation() {
        @Override
        public Station station() {
            return s;
        }

        @Override
        public Radio radio() {
            return r;
        }
    }  )).collect(groupingBy(RadioStation::radio, mapping(RadioStation::stations, toUnmodifiableList())));
person David Lilljegren    schedule 25.10.2019

Мы можем сохранить промежуточный этап сбора g в карту, преобразовав непосредственно в поток SimpleEntry, например:

Map<Long, Station> result = stationList.stream()
                .flatMap(station -> station.getRadioList().stream().map(radio -> new SimpleEntry<>(radio, station)))
                .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue));
person Dani Mesejo    schedule 26.10.2019

Вы можете сделать это без Streams, конечно, что, вероятно, сделает его немного более читабельным.

Map<Radio, Station> LOOK_UP = new HashMap<>();
List<Station> stations = ...


stations.forEach(station -> {
    station.getRadios().forEach(radio -> {
         LOOK_UP.put(radio, station);
    });
});

Это не сильно отличается от обычного цикла с:

for (Station station : stations) {
     for (Radio radio : station.getRadios()) {
          LOOK_UP.put(radio, station);
     }
}

Очевидная проблема здесь заключается в том, что LOOK_UP::put всегда будет заменять значение для определенного ключа, скрывая тот факт, что у вас когда-либо были дубликаты. Например:

[StationA = {RadioA, RadioB}]
[StationB = {RadioB}]

Когда вы ищете RadioB - что вы должны получить в результате?

Если бы у вас мог быть такой сценарий, очевидным было бы изменить определение LOOK-UP и использовать Map::merge:

    Map<Radio, List<Station>> LOOK_UP = new HashMap<>();
    List<Station> stations = new ArrayList<>();

    stations.forEach(station -> {
        station.getRadios().forEach(radio -> {
            LOOK_UP.merge(radio,
                          Collections.singletonList(station),
                          (left, right) -> {
                              List<Station> merged = new ArrayList<>(left);
                              merged.addAll(right);
                              return merged;
                          });
        });
    });

Другая возможность - генерировать исключение, когда есть эти детские сопоставления:

stations.forEach(station -> {
       station.getRadios().forEach(radio -> {
            LOOK_UP.merge(radio, station, (left, right) -> {
                 throw new RuntimeException("Duplicate Radio");
            });
       });
 });

Проблема с этим последним фрагментом заключается в том, что вы не можете зарегистрировать radio, который следует обвинить в неуникальности. left и right это Stationsс. Если вы тоже этого хотите, вам нужно будет использовать слияние, которое не зависит от Map::merge внутри, как в этот ответ.

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

person Eugene    schedule 27.10.2019

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

это твой класс станции -

class Station {
public List<String> getRadioList() {
    return radioList;
}

private List<String> radioList = new ArrayList<>();
}

И список станций, которые вы хотите нанести на карту -

        List<Station> list = new ArrayList<>();

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

list.stream().collect(Collectors.flatMapping(station ->
                    station.getRadioList().stream()
                            .map(radio ->Map.entry( radio, station)),
            Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue), (radio, radio2) -> radio2)));
  1. Мы конвертируем их в Map.Entry
  2. Мы соберем их все вместе с сборщиком flatmapping.

Если вы не хотите использовать flatMapping, вы можете сначала использовать FlatMap, а затем собирать, это будет более читабельно.

list.stream().flatMap(station -> station.getRadioList().stream().map(s -> Map.entry(s, station)))
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (radio, radio2) -> radio2)));
person Anand Vaidya    schedule 29.10.2019