I have a input which is of type: List<List<String>>.
An example input:
[A, A1Name]
[B, B1Name]
I want to convert it to BiMap
A -> A1Name
B -> B1Name
What is the best way to achieve this:
Currently I am doing:
final BiMap<String, String> myMap = HashBiMap.create();
lines.forEach(
(tokens) -> {
myMap.put(tokens.get(0), tokens.get(1));
}
);
Since BiMap implements Map, you can use the toMap collector. To replicate the behavior of your loop (duplicate keys silently override values, duplicate values throw exception), you can do the following:
BiMap<String,String> m = lines.stream().collect(toMap(
x->x.get(0), x->x.get(1), (a,b)->b, HashBiMap::create
));
As an aside, when you are not sure how to convert your code to streams, 3-argument collect provides a way to convert the iterative code almost verbatim:
BiMap<String,String> m = lines.stream().collect(
HashBiMap::create,
(bm,t) -> bm.put(t.get(0), t.get(1)),
BiMap::putAll
);
Related
I have a Java stream that invokes .collect(Collectors.toMap). Collectors.toMap accepts a keyMapper and a valueMapper functions. I'd like to create two entries for each stream element, with two different keyMapper functions, but with the same valueMapper function. Is it possible to do this in a nice stream syntax without creating a custom collector?
Of course, I could also get one map, then add another set of keys with the same values to it, outside of the stream chain calls. But I was wondering if it could be made neater...
Basically what I have is:
List<A> someObjects = ...; // obtain somehow
Map<String, B> res = someObjects.stream().collect(Collectors.toMap(keyMapper1, valueMapper));
And functions keyMapper1 and keyMapper2 produce different strings, and I want both of those in my map with the same value.
What I can do is:
Map<A, B> map = someObjects.stream().collect(Collectors.toMap(Function.identity(), valueMapper));
Map<String, B> result = new HashMap<>();
map.forEach((a, b) -> {
result.put(keyMapper1(a), b);
result.put(keyMapper2(a), b);
});
But maybe something could be done without creating an intermediate variable?
You can use flatMap to create a stream of all the map entries first, and then collect them to a map. Something like this:
Map<String, String> map = someObjects.stream()
.flatMap(obj -> Stream.of(
Map.entry(keyMapper1(obj), valueMapper(obj)),
Map.entry(keyMapper2(obj), valueMapper(obj))))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
I have a problem understanding how to use ImmutableSortedMap.toImmutableSortedMap(), when I want to create a frequency map. I know about Multiset (asked about that previously and got excellent help), but I don't want to use it this time, because it will require me to write a custom serializer to create a json representation that works for the consumers of said json.
The below code works, i.e. it creates the desired frequency map, sorted on key in ascending order, but it uses a temporary map, which I then use to create the ImmutableSortedMap. I would like to get rid of the temporary map. My attempts to use toImmutableSortedMap() collector method for this scenario failed to produce code that even compiled...
I am using Java 8 and Guava version 28.1
#Test
public void test() {
Map<String, Long> intermediateMap = Stream.of("b", "a", "c", "b")
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
ImmutableSortedMap<String, Long> desiredMap = ImmutableSortedMap.copyOf(intermediateMap);
System.out.println(desiredMap); // Outputs {a=1, b=2, c=1}, which is the desired state
}
Map<String, Long> result =
Stream.of("b", "a", "c", "b")
.collect(ImmutableSortedMap.toImmutableSortedMap(
Comparator.naturalOrder(),
Function.identity(),
x -> 1L,
Long::sum
));
You can even achieve something similar (an unmodifiable, sorted Map), without using Guava.
Map<String, Long> immutableSortedMap = Stream.of("b", "a", "c", "b")
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(Function.identity(), TreeMap::new, Collectors.counting()),
Collections::unmodifiableMap)
);
Use a TreeMap to achieve the sorting (on natural order)
Use Collectors::collectingAndThen to wrap the result in an unmodifiable map
I would like to convert my map which looks like this:
{
key="someKey1", value=Apple(id="1", color="green"),
key="someKey2", value=Apple(id="2", color="red"),
key="someKey3", value=Apple(id="3", color="green"),
key="someKey4", value=Apple(id="4", color="red"),
}
to another map which puts all apples of the same color into the same list:
{
key="red", value=list={apple1, apple3},
key="green", value=list={apple2, apple4},
}
I tried the following:
Map<String, Set<Apple>> sortedApples = appleMap.entrySet()
.stream()
.collect(Collectors.toMap(l -> l.getColour, ???));
Am I on the right track? Should I use filters for this task? Is there an easier way?
Collectors.groupingBy is more suitable than Collectors.toMap for this task (though both can be used).
Map<String, List<Apple>> sortedApples =
appleMap.values()
.stream()
.collect(Collectors.groupingBy(Apple::getColour));
Or, to group them into Sets use:
Map<String, Set<Apple>> sortedApples =
appleMap.values()
.stream()
.collect(Collectors.groupingBy(Apple::getColour,
Collectors.mapping(Function.identity(),
Collectors.toSet())));
or (as Aomine commented):
Map<String, Set<Apple>> sortedApples =
appleMap.values()
.stream()
.collect(Collectors.groupingBy(Apple::getColour, Collectors.toSet()));
if you want to proceed with toMap you can get the result as follows:
map.values() // get the apples
.stream() // Stream<Apple>
.collect(toMap(Apple::getColour, // group by colour
v -> new HashSet<>(singleton(v)), // have values as set of apples
(l, r) -> {l.addAll(r); return l;})); // merge colliding apples by colour
stream over the map values instead of entrySet because we're not concerned with the map keys.
Apple::getColour is the keyMapper function used to extract the "thing" we wish to group by, in this case, the Apples colour.
v -> new HashSet<>(singleton(v)) is the valueMapper function used for the resulting map values
(l, r) -> {l.addAll(r); return l;} is the merge function used to combine two HashSet's when there is a key collision on the Apple's colour.
finally, the resulting map is a Map<String, Set<Apple>>
but this is better with groupingBy and toSet as downstream:
map.values().stream().collect(groupingBy(Apple::getColour, toSet()));
stream over the map values instead of entrySet because we're not concerned with the map keys.
groups the Apple's by the provided classification function i.e. Apple::getColour and then collect the values in a Set hence the toSet downstream collector.
finally, the resulting map is a Map<String, Set<Apple>>
short, readable and the idiomatic approach.
You could also do it without a stream:
Map<String, Set<Apple>> res = new HashMap<>();
map.values().forEach(a -> res.computeIfAbsent(a.getColour(), e -> new HashSet<>()).add(a));
iterate over the map values instead of entrySet because we're not concerned with the map keys.
if the specified key a.getColour() is not already associated with a value, attempts to compute its value using the given mapping function e -> new HashSet<>() and enters it into the map. we then add the Apple to the resulting set.
if the specified key a.getColour() is already associated with a value computeIfAbsent returns the existing value associated with it and then we call add(a) on the HashSet to enter the Apple into the set.
finally, the resulting map is a Map<String, Set<Apple>>
You can use Collectors.groupingBy and Collectors.toSet()
Map<String, Set<Apple>> sortedApples = appleMap.values() // Collection<Apple>
.stream() // Stream<Apple>
.collect(Collectors.groupingBy(Apple::getColour, // groupBy colour
Collectors.mapping(a -> a, Collectors.toSet()))); // collect to Set
You've asked how to do it with streams, yet here's another way:
Map<String, Set<Apple>> result = new LinkedHashMap<>();
appleMap.values().forEach(apple ->
result.computeIfAbsent(apple.getColor(), k -> new LinkedHashSet<>()).add(apple));
This uses Map.computeIfAbsent, which either returns the set mapped to that color or puts an empty LinkedHashSet into the map if there's nothing mapped to that color yet, then adds the apple to the set.
EDIT: I'm using LinkedHashMap and LinkedHashSet to preserve insertion order, but could have used HashMap and HashSet, respectively.
Consider the a list as id1_f, id2_d, id3_f, id1_g, how can I use stream to get a reduced map in format of <String, Integer>of statistics like:
id1 2
id2 1
id3 1
Note: the key is part before _. Is reduce function can help here?
This will get the job done:
Map<String, Long> map = Stream.of("id1_f", "id2_d", "id3_f", "id1_g")
.collect(
Collectors.groupingBy(v -> v.split("_")[0],
Collectors.counting())
);
You can also use the toMap collector:
myList.stream()
.collect(Collectors.toMap((String s) -> s.split("_")[0],
(String s) -> 1, Math::addExact);
if you care about the order of the elements then dump the result into a LinkedHashMap.
myList.stream()
.collect(Collectors.toMap((String s) -> s.split("_")[0],
(String s) -> 1, Math::addExact,
LinkedHashMap::new));
A non-stream approach using Map::merge:
Map<String, Integer> result = new LinkedHashMap<>();
myList.forEach(s -> result.merge(s.split("_")[0], 1, Math::addExact));
Since you want to count the elements, I'd suggest using Guava's Multiset interface, which is dedicated to such purpose.
The definition of Multiset from its JavaDoc:
A collection that supports order-independent equality, like Set, but may have duplicate elements. A multiset is also sometimes called a bag.
Elements of a multiset that are equal to one another are referred to as occurrences of the same single element. The total number of occurrences of an element in a multiset is called the count of that element.
Here are two ways to use it:
1) Without the Stream API:
ImmutableMultiset<String> multiset2 = ImmutableMultiset.copyOf(Lists.transform(
list, str -> StringUtils.substringBefore(str, "_")
));
2) Using the Stream API:
ImmutableMultiset<String> multiset = list.stream()
.map(str -> StringUtils.substringBefore(str, "_"))
.collect(ImmutableMultiset.toImmutableMultiset());
Note that instead of using something like s.split("_")[0], I used Apache Commons Lang's StringUtils.substringBefore, which I find much more readable.
You retrieve the counts of the elements using Multiset.count() method.
I am trying to collect into a ListMultiMap using java 8 without using the forEach operation.
If I were to write the code in Java 7, it will be something like this:
ListMultimap<String, String> result = ArrayListMultimap.create();
for(State state: states) {
for(City city: state.getCities()) {
result.put(state.getName(), city.getName());
}
}
I found online a website that talks about creating your own collectors to use in scenarios such as this one.
I used this implementation for the collector. I then wrote the following code:
ListMultimap<String, String> result = states
.stream()
.flatMap(state -> state.getCities().stream()
.map(city -> {
return new Pair(state, city);
}))
.map(pair -> {
return new Pair(pair.first().getName(), pair.second().getName()));
})
.collect(MultiMapCollectors.listMultimap(
Pair::first,
Pair::second
)
);
But at the collect level, I can only pass just one parameter, and I can seem to find a way to pass two parameters.
Following the example from the website, I understood that to use both, I need to store a "pair" in the multimap such as the following:
ArrayListMultimap<String, Pair> testMap = testObjectList.stream().collect(MultiMapCollectors.listMultimap((Pair p) -> p.first().getName()));
However this is not what I'm looking for, I want to collect into ListMultimap using the state's name and the city's name using java 8's collector (and no forEach).
Can someone help me with that ?
Thank you!
ImmutableListMultimap.flatteningToImmutableListMultimap
return states.stream()
.collect(flatteningToImmutableListMultimap(
State::getName,
state -> state.getCities().stream().map(City::getName)));
You could create a custom collector for that (note that Louis Wasserman's answer will do a forEachOrdered internally, you can't really escape a forEach either internally or externally).
ListMultimap<String, String> list = states.collect(Collector.of(ArrayListMultimap::create, (multimap, s) -> {
multimap.putAll(s.getName(), s.getCities().stream().map(City::getName).collect(Collectors.toList()));
}, (multi1, multi2) -> {
multi1.putAll(multi2);
return multi1;
}));