8

I have a list of Station, in each Station there is a list of radios. I need to create a lookup Map of radio to Station. I know how to use Java 8 stream forEach to do it:

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

But I believe there should be more concise way like using Collectors.mapping().

Anyone can help?

Naman
  • 27,789
  • 26
  • 218
  • 353
Nico
  • 81
  • 3
  • Is it guaranteed that no radio maps to more than one station? – slim Apr 06 '17 at 14:20
  • 1
    `stationList.stream().forEach(station -> station.getRadioList().stream().forEach(rl -> radioToStationMap.put(rl, station)))` not good enough for you? – ZhenyaM Apr 06 '17 at 14:27
  • 3
    @ZhenyaM It's better to use stream operations and a `collect` than to use `forEach` to write to a mutable list. – RealSkeptic Apr 06 '17 at 14:28
  • Possible duplicate of [Convert List of List of Object to a map - using lambdas in java 8](http://stackoverflow.com/questions/40213731/convert-list-of-list-of-object-to-a-map-using-lambdas-in-java-8) – Mohamed Gad-Elrab Apr 06 '17 at 15:26
  • 4
    I’m pretty sure the most “concise way” is not to use lambdas or Streams at all. – VGR Apr 06 '17 at 16:19
  • @EricWilson, which part is _very convoluted_? I understood the answer in a single read. I do agree it might require a bit more details, but other than that - it is a good one. – Eugene Oct 27 '19 at 18:53

9 Answers9

10

This should work and you don't need third parties.

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));
utkusonmez
  • 1,486
  • 15
  • 22
4

Based on the question, considering the entities Radio and Station to be defined as:

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

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

One can create a lookup map from a List<Station> as an input using a utility such as:

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));
}

Slightly different from this behaviour, if for same(equal) Radio element across multiple Stations, one wants to group all such stations, it can be achieved using groupingBy instead of toMap such as :

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())));
}
Naman
  • 27,789
  • 26
  • 218
  • 353
1

If you are open to using a third-party library, there is the method groupByEach from Eclipse Collections:

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

This can also be written using Java 8 Streams with the Collectors2 utility from Eclipse Collections:

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

Note: I am a committer for Eclipse Collections.

Donald Raab
  • 6,458
  • 2
  • 36
  • 44
0

I don't think that you can do it in more concise way using Collectors, as compared to mixed solution like

    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);
        });
    });

(you can call .forEach directly on collections, don't need to go through .stream())

Shortest fully 'functional' solution I was able to come up with would be something like

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

using any of the Pair classes available in third party libraries. Java 8 is very verbose for simple operations, compared to dialects like Xtend or Groovy.

Artur Biesiadowski
  • 3,595
  • 2
  • 13
  • 18
  • If you have duplicates in `radio`, you'll get `java.lang.IllegalStateException: Duplicate key`. To prevent it you can add merger like `Collectors.toMap(Pair::getKey, Pair::getValue, (station, station2) -> station2)` in your solution – Vlad Bochenin Apr 06 '17 at 15:09
  • 1
    If there are duplicates, Multimap should be probably used instead. – Artur Biesiadowski Apr 06 '17 at 15:12
0

How about:

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

By StreamEx

user_3380739
  • 1
  • 14
  • 14
0

Just as in Artur Biesiadowski answer i think you must create a list of pair's and then group them, at least if you want to cater for the case that the radios are not unique per station.

In C# you have practical anonymous classes one can use for this, but in Java you would have to define at least the interface of the Pair class

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())));
David Lilljegren
  • 1,799
  • 16
  • 19
0

We can save the intermediate step of collectiong to a Map by transforming directly to a Stream of SimpleEntry, for example:

Map<Long, Station> result = stationList.stream()
                .flatMap(station -> station.getRadioList().stream().map(radio -> new SimpleEntry<>(radio, station)))
                .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue));
Dani Mesejo
  • 61,499
  • 6
  • 49
  • 76
0

You can do it without Streams, of course, probably making it a bit more readable.

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


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

This is not very different than a plain loop with:

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

The obvious problem here is that LOOK_UP::put will always replace the value for a certain key, hiding the fact that you ever had duplicates. For example:

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

When you search for RadioB - what should you get as a result?

If you could have such a scenario, the obvious thing is to change the LOOK-UP definition and use 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;
                          });
        });
    });

Another possibility is to throw an Exception when there are these kid of mappings:

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

The problem with this last snippet, is that you can't really log the radio that is to be blamed for non-uniqueness. left and right are Stationss. If you want that too, you will need to use a merger that does not rely on Map::merge internally, like in this answer.

So you can see, that it all depends on how and what exactly you need to handle.

Eugene
  • 117,005
  • 15
  • 201
  • 306
0

Turns out to be a little different answer, but we can do it using flatMapping collector provided with Java9.

this is your station class -

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

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

And the list of stations you want to map -

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

Below is the code that will let you map it using flatMapping collector.

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. We will convert them to Map.Entry
  2. We will collect all of them together with flatmapping collector

If you don't want to use flatMapping, you can actually first use FlatMap and then collect, it will be more readable.

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)));
Anand Vaidya
  • 1,374
  • 11
  • 26