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:
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 Station
s, 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())));
}
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.
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));
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 Stations
s. 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.
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.
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));