How to group by keys of stream of pairs - java

Say that I have the following stream:
...
import javafx.util.Pair;
...
Pair[] testPairs = {
new Pair<>("apple", "James"),
new Pair<>("banana", "John"),
new Pair<>("grapes", "Tom"),
new Pair<>("apple", "Jenkins"),
new Pair<>("banana", "Edward"),
new Pair<>("grapes", "Pierre")
};
Map<String, List<String>> result1 = Arrays.stream(testPairs)...;
Map<String, String> result2 = Arrays.stream(testPairs)...;
For result1, I want to group by keys of the pairs and get all the correspondant names.
For result2, I want to group by keys and get whichever in the list of strings (of the previous result).
How is it possible to achieve that by using java 8 stream api ?

You may do it like so,
Map<String, List<String>> result1 = Arrays.stream(testPairs)
.collect(Collectors.groupingBy(Pair::getS,
Collectors.mapping(Pair::getT, Collectors.toList())));
Map<String, String> result2 = Arrays.stream(testPairs)
.collect(Collectors.toMap(Pair::getS, Pair::getT, (v1, v2) -> v1));
If you are using row types with arrays, here's the version with the necessary casts as specified by the below comment by YCF_L.
Map<String, List<String>> result1 = Arrays.stream(testPairs)
.collect(Collectors.groupingBy(p -> (String) p.getKey(),
Collectors.mapping(p -> (String) p.getValue(), Collectors.toList())));
Map<String, String> result2 = Arrays.stream(testPairs)
.collect(Collectors.toMap(
p -> (String) p.getKey(),
p -> (String) p.getValue(),
(a, b) -> b)
);
}

Related

How to Merge the Duplicate Values and its Keys present in the HashMap

HashMap<String, String> map = new HashMap<String, String>();
HashMap<String, String> newMap = new HashMap<String, String>();
map.put("A","1");
map.put("B","2");
map.put("C","2");
map.put("D","1");
Expected Output: "AD", "1" and "BC", "2" present inside the newMap which means, if the data values were same it needs combine its keys to have only one data value by combining its keys inside the newMap created how to achieve this in Java?
You want to group by the "integer" value using Collectors.groupingBy and collect the former keys as a new value. By default, grouping yields in List. You can further use downstream collector Collectors.mapping and another downstream collector Collectors.reducing to map and concatenate the individual items (values) as a single String.
Map<String, String> groupedMap = map.entrySet().stream()
.collect(Collectors.groupingBy(
Map.Entry::getValue,
Collectors.mapping(
Map.Entry::getKey,
Collectors.reducing("", (l, r) -> l + r))));
{1=AD, 2=BC}
Now, you can switch keys with values for the final result, though I really think you finally need what is already in the groupedMap as further processing might cause an error on duplicated keys:
Map<String, String> newMap = groupedMap.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getValue,
Map.Entry::getKey));
{BC=2, AD=1}
It is possible, put it all together using Collectors.collectingAndThen (matter of taste):
Map<String, String> newMap = map.entrySet().stream()
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(
Map.Entry::getValue,
Collectors.mapping(
Map.Entry::getKey,
Collectors.reducing("", (l, r) -> l + r))),
m -> m.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getValue,
Map.Entry::getKey))));
Based on logic:
Loop through your map
For each value, get the corresponding key from the new map (based on the value)
If the new map key exists, remove it and put it again with the extra letter at the end
If not exists, just put it without any concatenation.
for (var entry : map.entrySet())
{
String newMapKey = getKey(newMap, entry.getValue());
if (newMapKey != null)
{
newMap.remove(newMapKey);
newMap.put(newMapKey + entry.getKey(), entry.getValue());
continue;
}
newMap.put(entry.getKey(), entry.getValue());
}
The extra method:
private static String getKey(HashMap<String, String> map, String value)
{
for (String key : map.keySet())
if (value.equals(map.get(key)))
return key;
return null;
}
{BC=2, AD=1}
Using Java 8
You can try the below approach in order to get the desired result.
Code:
public class Test {
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<>();
Map<String, String> newMap;
map.put("A","1");
map.put("B","2");
map.put("C","2");
map.put("D","1");
Map<String, String> tempMap = map.entrySet().stream()
.collect(Collectors.groupingBy(Map.Entry::getValue,
Collectors.mapping(Map.Entry::getKey,Collectors.joining(""))));
newMap = tempMap.entrySet().stream().sorted(Map.Entry.comparingByValue())
.collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey,(a,b) -> a, LinkedHashMap::new));
System.out.println(newMap);
}
}
Output:
{AD=1, BC=2}
If you want the keys of the source map to be concatenated in alphabetical order like in your example "AD", "BC" (and not "DA" or "CB"), then you can ensure that by creating an intermediate map of type Map<String,List<String>> associating each distinct value in the source map with a List of keys. Then sort each list and generate a string from it.
That how it might be implemented:
Map<String, String> map = Map.of(
"A", "1", "B", "2","C", "2","D", "1"
);
Map<String, String> newMap = map.entrySet().stream()
.collect(Collectors.groupingBy( // intermediate Map<String, List<String>>
Map.Entry::getValue,
Collectors.mapping(Map.Entry::getKey, Collectors.toList())
))
.entrySet().stream()
.collect(Collectors.toMap(
e -> e.getValue().stream().sorted().collect(Collectors.joining()),
Map.Entry::getKey
));
newMap.forEach((k, v) -> System.out.println(k + " -> " + v));
Output:
BC -> 2
AD -> 1

Java streams - Map<key, List<VO>> to Map<key, List<String from VO>

I've a Map<KeyString, List<MyVO>>.
MyVO.java contains:
String name;
int id;
I want to map it into Map<KeyString, List<names from MyVO>.
How can achieve this using java 8 streams?
You can use something like this:
Map<String, List<String>> response =
map.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().stream()
.map(MyVO::getName)
.collect(Collectors.toList())));
I'm using a record to demo. A class would also work here.
record VO(String getStr)
}
first create some data
Map<String, List<VO>> map =
Map.of("A", List.of(new VO("S1"), new VO("S2")), "B",
List.of(new VO("S3"), new VO("S4")));
map.entrySet().forEach(System.out::println);
prints
A=[VO[str=S1], VO[str=S2]]
B=[VO[str=S3], VO[str=S4]]
now stream the entry set of the original map
and collect using Collectors.toMap.
use the orginal key from the Entry.
and stream the list of VO to pull out the string and create a new list.
Map<String,List<String>> result = map.entrySet().stream()
.collect(Collectors.toMap(
Entry::getKey,
e -> e.getValue().stream().map(VO::getStr).toList()));
prints
A=[S1, S2]
B=[S3, S4]
A solution:
public static void mapNames() {
final Map<String, List<MyVO>> voMap = new HashMap<>();
voMap.put("all", Arrays.asList(
new MyVO(1, "John"),
new MyVO(2, "Bill"),
new MyVO(3, "Johanna")
));
final Map<String, List<String>> nameMap = voMap.entrySet().stream()
.filter(Objects::nonNull)
.collect(
Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().stream()
.map(MyVO::getName)
.collect(Collectors.toList())
));
System.out.println(nameMap);
}
Output:
{all=[John, Bill, Johanna]}

Could you help me to merge values of several maps?

I'm trying to do the following modification:
final Map<String, List<Map<String, String>>> scopes = scopeService.fetchAndCacheScopesDetails();
final Map<String, Map<String, String>> scopesResponse = scopes.entrySet().stream().collect
(Collectors.toMap(Map.Entry::getKey, e -> e.getValue()
.stream().collect(Collectors.toMap(s -> (String) s.get(SCOPE_NM), s -> (String) s.get(SCOPE_ID))))
);
But I face "Duplicate key" error, so I'd like to change scopeResponses to Map<String, Map<String, List<String>>>
Could you tell me how to merge values s -> (String) s.get(SCOPE_ID) into a List or Set in this situation?
You need to create a Set for the value of the inner Map, and supply a merge function:
final Map<String, Map<String, Set<String>>> scopesResponse = scopes.entrySet().stream().collect
(Collectors.toMap(Map.Entry::getKey, e -> e.getValue()
.stream().collect(Collectors.toMap(s -> s.get(SCOPE_NM),
s -> {Set<String> set= new HashSet<>(); set.add(s.get(SCOPE_ID)); return set;},
(s1,s2)->{s1.addAll(s2);return s1;}))));
Or, you can construct the inner Map with groupingBy:
final Map<String, Map<String, Set<String>>> scopesResponse2 = scopes.entrySet().stream().collect
(Collectors.toMap(Map.Entry::getKey, e -> e.getValue()
.stream().collect(Collectors.groupingBy(s -> s.get(SCOPE_NM),
Collectors.mapping(s -> s.get(SCOPE_ID),Collectors.toSet())))));
You can also do it using Guava's ListMultimap (multimap is like a map of lists):
Map<String, ListMultimap<String, String>> scopesResponse = scopes.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> toMultimap(e)));
where
static ImmutableListMultimap<String, String> toMultimap(
Map.Entry<String, List<Map<String, String>>> entry) {
return entry.getValue().stream().collect(ImmutableListMultimap.toImmutableListMultimap(
s -> (String) s.get(SCOPE_NM),
s -> (String) s.get(SCOPE_ID)
));
}
If the values in the lists turn out to be duplicated, and you don't want that, use SetMultimap instead.

List of Strings to hashmap using Streams

I have a "," separated String array like this
a b c d,
f b h j,
l p o i,
I would like this to be converted to a Hashmap like
HashMap<String, List<String>> such that second element in list (delimited by space becomes key and the 3rd element becomes value)
So,
This should become
b -> c,h
p -> o
I want to use Streams API and I think this is the way to go:
List<String> entries = new ArrayList<>();
HashMap<String, List<String>> map = new HashMap<>();
HashMap<String, List<String>> newMap = entries.stream()
.collect(line -> {
if (map.contains(line.split(" ")[1])) {
// Get existing list and add the element
map.get(line.split(" ")[1].add(line.split(" ")[1]));
} else {
// Create a new list and add
List<String> values = new ArrayList<>();
values.add(line.split(" ")[1]);
map.put(line.split(" ")[0], values);
}
});
Is there any better way? How exactly should I return Hashmap from collect function?
You can use the Collectors.groupingBy as shown below to group the inputs (follow the inline comments):
String[] inputs = {"a b c d,", "f b h j,", "l p o i,"};
Map<String, List<String>> results =
Arrays.stream(inputs).map(s -> s.split(" ")).//splt with space
collect(Collectors.groupingBy(arr -> arr[1], // Make second element as the key
Collectors.mapping(arr -> arr[2], // Make third element as the value
Collectors.toList())));//collect the values to List
System.out.println(results);
Output:
{p=[o], b=[c, h]}
I suggest you read the API here to understand how Collectors.groupingBy along with Collectors.mappingworks.
You can achieve the task at hand using a groupingBy collector along with Collectors.mapping as a downstream collector.
Map<String, List<String>> collect =
myList.stream()
.map(s -> s.split(" "))
.collect(Collectors.groupingBy(a -> a[1],
Collectors.mapping(a -> a[2], Collectors.toList())));
output:
{p=[o], b=[c, h]}
if you want to maintain insertion order then you can specify a LinkedHashMap like this:
Map<String, List<String>> collect =
myList.stream()
.map(s -> s.split(" "))
.collect(Collectors.groupingBy(s -> s[1],
LinkedHashMap::new,
Collectors.mapping(s -> s[2], Collectors.toList())));
output:
{b=[c, h], p=[o]}
If you want HashMap , not just any Map
HashMap<String, List<String>> output =myList.stream().map(s -> s.split(" "))
.collect(Collectors.groupingBy((s) -> s[1],
HashMap::new,
Collectors.mapping(
(s) -> s[2],
Collectors.toList())));

Java 8 - stream convert map's value type

I would like to convert type List<A> to List<B>. can I do this with java 8 stream method?
Map< String, List<B>> bMap = aMap.entrySet().stream().map( entry -> {
List<B> BList = new ArrayList<B>();
List<A> sList = entry.getValue();
// convert A to B
return ???; Map( entry.getKey(), BList) need to return
}).collect(Collectors.toMap(p -> p.getKey(), p -> p.getValue()));
I tried with this code, but cannot convert it inside map().
If I understood it correctly you have a Map<String, List<A>> and you want to convert it to a Map<String, List<B>>. You can do something like:
Map<String, List<B>> result = aMap.entrySet().stream()
.collect(Collectors.toMap(
entry -> entry.getKey(), // Preserve key
entry -> entry.getValue().stream() // Take all values
.map(aItem -> mapToBItem(aItem)) // map to B type
.collect(Collectors.toList()) // collect as list
);
You can instantiate AbstractMap.simpleEntry in the map function and perform the transformation.
E.g. the following code converts List<Integer> to List<String>:
Map<String, List<Integer>> map = new HashMap<>();
Map<String, List<String>> transformedMap = map.entrySet()
.stream()
.map(e -> new AbstractMap.SimpleEntry<String, List<String>>(e.getKey(), e.getValue().stream().map(en -> String.valueOf(en)).collect(Collectors.toList())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
You may do it like this:
public class Sandbox {
public static void main(String[] args) {
Map<String, List<A>> aMap = null;
Map<String, List<B>> bMap = aMap.entrySet().stream().collect(toMap(
Map.Entry::getKey,
entry -> entry.getValue().stream()
.map(Sandbox::toB)
.collect(toList())));
}
private static B toB(A a) {
// add your conversion
return null;
}
class B {}
class A {}
}

Categories

Resources