Reputation: 101
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
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
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 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())));
}
Upvotes: 4
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
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)));
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
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 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.
Upvotes: 1
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
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
Reputation: 1254
How about:
radioToStationMap = StreamEx.of(stationList)
.flatMapToEntry(s -> StreamEx.of(s.getRadioList()).mapToEntry(r -> s).toMap())
.toMap();
By StreamEx
Upvotes: 0
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