Nico
Nico

Reputation: 101

Java 8 convert List to Lookup Map

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?

Upvotes: 10

Views: 2126

Answers (9)

utkusonmez
utkusonmez

Reputation: 1506

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

Upvotes: 10

Naman
Naman

Reputation: 31928

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

Upvotes: 4

Donald Raab
Donald Raab

Reputation: 6686

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.

Upvotes: 1

Anand Vaidya
Anand Vaidya

Reputation: 1461

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

Upvotes: 0

Eugene
Eugene

Reputation: 120868

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.

Upvotes: 1

Dani Mesejo
Dani Mesejo

Reputation: 61910

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

Upvotes: 0

David Lilljegren
David Lilljegren

Reputation: 1939

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

Upvotes: 0

user_3380739
user_3380739

Reputation: 1254

How about:

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

By StreamEx

Upvotes: 0

Artur Biesiadowski
Artur Biesiadowski

Reputation: 3698

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.

Upvotes: 0

Related Questions