Merge list of maps with duplicate keys [duplicate] - java

This question already has answers here:
Merge map of arrays with duplicate keys
(8 answers)
Merging a list of maps into a single map
(5 answers)
Closed 2 years ago.
I have a list of HashMap<Integer, ArrayList<String>> and would like to merge them in a loop.
The problem is that each map's key starts from 0 so the keys will be duplicated. The putAll() does not work as it overrides the keys and always gives me the last map.
I have seen examples of merging two maps by using Stream but in my case, there might be more than 2 maps. I am trying to generate a merged map that has an incremental key. For example:
Let's say I have 2 maps in the list (could be more) and both keys start from 0 but ends at different values.
1st map, the key starts at 0 ends at 10
2nd map, the key starts at 0 ends at 15
Is it possible to add the second map with the key starting at 11?
In the end, I need a merged map in which the first key starts at 0 and the last key ends at 25.

Assuming you have a list of maps, where the keys of each map are integers in range [0-k], [0-n], [0, r] ... and your resulting map should a key set in the range of [0 - (k+n+r..)] something like below should work:
public static void main(String[] args) throws IOException {
//example list of maps
List<Map<Integer,List<String>>> mapList = List.of(
Map.of( 0,List.of("foo","foo"),
1,List.of("bar","bar"),
2,List.of("baz","baz")),
Map.of( 0,List.of("doo","doo"),
1,List.of("gee","gee"),
2,List.of("woo","woo")),
Map.of( 0,List.of("cab","cab"),
1,List.of("kii","kii"),
2,List.of("taa","taa"))
);
AtomicInteger ai = new AtomicInteger();
Map<Integer,List<String>> result =
mapList.stream()
.flatMap(map -> map.values().stream())
.collect(Collectors.toMap(list -> ai.getAndIncrement(), Function.identity()));
result.forEach((k,v) ->{
System.out.println(k + " : " + v);
});
}

I'd iterate over any number of maps you have, and then for each map you want to combine, iterate over the entries. For each entry you can use computeIfAbsent to conditionally create an empty list for the key, and then call addAll on the value. E.g.:
List<Map<Integer, List<String>>> maps = List.of(
Map.of(1, List.of("hello")),
Map.of(2, List.of("world")),
Map.of(1, List.of("a"), 2, List.of("b"))
);
Map<Integer, List<String>> combined = new HashMap<>();
for (Map<Integer, List<String>> map : maps) {
for (Map.Entry<Integer, List<String>> e : map.entrySet()) {
combined.computeIfAbsent(e.getKey(), k -> new ArrayList<>()).addAll(e.getValue());
}
}

If you prefer a streams approach
Map<Integer, List<String>> m1;
Map<Integer, List<String>> m2;
Map<Integer, List<String>> m3;
Map<Integer, List<String>> combined = new HashMap<>();
Stream
.of(m1, m2, m3)
.flatMap(m -> m.entrySet().stream())
.forEach(e -> combined.computeIfAbsent(e.getKey(), k -> new ArrayList<>())
.addAll(e.getValue()));

I know you asked for a map, but based on your explanation your ultimate solution and the fact that your keys are now sequential integers starting at 0, you could create just a List<List<String>>. In that case, you could do it like this:
List<List<String>> result = mapList.stream()
.flatMap(map->map.values().stream())
.collect(Collectors.toList());

Related

Store multiple values for a key in Collectors.toMap() in Java [duplicate]

This question already has answers here:
Java stream/collect: map one item with multiple fields to multiple keys
(1 answer)
HashMap with multiple values under the same key
(21 answers)
Closed 1 year ago.
This is how my map looks like.
Map<String, String> maps = List.stream().collect(Collectors.toMap(Cat::getName, Cat::getNumber, (c1,c2) -> c1));
If I already have "Lucy, 101" in maps then I am unable to add another value corresponding to the name Lucy i.e "Lucy, 102". Is there any way to change the merge function [ (c1,c2) -> c1 ] so that I can have two values corresponding to a single key (Lucy) in my maps?
By your requirement, in order to allow multiple values for the same key, you need to implement the result as Map<String, Collection<String>>.
In your case, you can merely use groupingBy + mapping collectors instead of toMap with the merge function:
Map<String, List<String>> maps = List.stream()
.collect(Collectors.groupingBy(
Cat::getName,
Collectors.mapping(Cat::getNumber, Collectors.toList())
);
However, you may want to consider a merge function as joining strings:
Map<String, String> maps = List.stream()
.collect(Collectors.toMap(
Cat::getName,
Cat::getNumber, (num1, num2) -> String.join("; ", num1, num2)
);
Then the map would contain "compound" values in the form of a string.
Since the type of your map is Map<String, String>, it can only return one string. However, you expect to get multiple strings from get. You need to change the type of the map to e.g. Map<String, List<String>> or Map<String, Collection<String>>.
Then, you can use groupingBy like this:
Map<String, List<String>> map = yourList.stream().collect(Collectors.groupingBy(
Cat::getName, // group by the name
Collectors.mapping( // map each group of cats to their numbers
Cat::getNumber, Collectors.toList() // collect each group to a list
)
));
If you are okay with multiple numbers in the same string (e.g. "101 102"), you can use Collectors.joining:
Map<String, String> map = yourList.stream().collect(Collectors.groupingBy(
Cat::getName,
Collectors.mapping(
Cat::getNumber, Collectors.joining(" ")
)
));

How to group nested maps

My current attempt:
Map<Integer, Map<String, Integer>> collect = shopping.entrySet()
.stream()
.collect(Collectors.toMap/*groupingBy? */(e -> e.getKey().getAge(),
e -> e.getValue().entrySet().stream().collect(Collectors.groupingBy(b -> b.getKey().getCategory(), Collectors.summingInt(Map.Entry::getValue)))));
shopping is basically a map: Map<Client, Map<Product,Integer>>,
The problem comes from the fact that the provided data contains multiple values by key - there are Clients with same ages, and the code works only for a single value by key.
How could I make this code work also for multiple keys?
I suppose it should be somehow changed to use collect collect(Collectors.groupingBy) ->
in the resulting map Map<Integer, Map<String, Integer>>:
The outer key (Integer) represents the client age.
The inner key (String) - represents product category
The inner maps value (Integer) - represents the number of products
which belong to a specific category.
My attempt using groupingBy:
Map<Integer, Map<String, Integer>> collect = shopping.entrySet()
.stream()
.collect(Collectors.groupingBy(/*...*/))
Simply I want to refactor that code into one using streams:
Map<Integer, Map<String, Integer>> counts = new HashMap<>();
for (Map.Entry<Client, Map<Product, Integer>> iData : shopping.entrySet()) {
int age = iData.getKey().getAge();
for (Map.Entry<Product, Integer> iEntry : iData.getValue().entrySet()) {
String productCategory = iEntry.getKey().getCategory();
counts.computeIfAbsent(age, (agekey) -> new HashMap<>()).compute(productCategory, (productkey, value) -> value == null ? 1 : value + 1);
}
}
A non-stream(forEach) way to convert your for loop could be :
Map<Integer, Map<String, Integer>> counts = new HashMap<>();
shopping.forEach((key, value1) -> value1.keySet().forEach(product ->
counts.computeIfAbsent(key.getAge(),
(ageKey) -> new HashMap<>())
.merge(product.getCategory(), 1, Integer::sum)));
This would be more appropriate via a groupingBy collector instead of toMap.
Map<Integer, Map<String, Integer>> result = shopping.entrySet()
.stream()
.collect(groupingBy(e -> e.getKey().getAge(),
flatMapping(e -> e.getValue().keySet().stream(),
groupingBy(Product::getCategory,
summingInt(e -> 1)))));
note this uses flatMapping which is only available in the standard library as of jdk9.

Get map entries with matching keys using Stream API

I have an array of values
String [] str = { ONE, TWO, THREE };
and some key-value pairs stored in a map
Map<String, String> map;
How can I use the Stream API to get all entries from the map whose key is contained in the array?
I assume that when you say you want the entries from this map, you mean the values
so here is how I would do it
String[] str = {"ONE", "TWO", "THREE"};
Map<String, String> map = <Your map's here>;
List<String> values = Stream.of(str)
.filter(map::containsKey)
.map(map::get)
.collect(Collectors.toList());
If the map and the set of keys to remove are small, start with a copy of the map, and then modify it.
Map<String, String> filtered = new HashMap<>(map);
filtered.keySet().retainAll(Arrays.asList(str));
If the map is so much larger than the set of keys to remove that making a copy is undesirable, you can build a new map with only the correct entries:
Set<String> filter = new HashSet<>(Arrays.asList(str));
Map<String, String> filtered = map.entrySet().stream()
.filter(e -> filter.contains(e.getKey))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
You didn’t specify the form of the result.
If you want the same map but only containing the specified keys, you can use
Map<String, String> map;
Map<String, String> result = new HashMap<>(map);
result.keySet().retainAll(Arrays.asList(str));
If the old state is not needed anymore and the map is mutable, you may apply the second statement to the map without creating a copy.
A solution always creating a new Map would be
Map<String, String> result = Arrays.stream(str)
.collect(HashMap::new, (m, k) -> m.computeIfAbsent(k, map::get), Map::putAll);
If you just want an array containing values corresponding to the array containing the keys, you could use
String[] result = new String[str.length];
Arrays.setAll(result, ix -> map.get(str[ix]));
or
String[] result = Arrays.stream(str).map(map::get).toArray(String[]::new);
One way is to stream the array and collect the elements into a new map, with the keys being the elements and the values taken from your map for each key:
Map<String, String> result = Arrays.stream(str)
.collect(Collectors.toMap(e -> e, map::get));
This assumes all the elements of the array are keys in the map, and no duplicate elements on the array.
If you want to filter out elements that aren't keys of the map, use filter on the stream:
Map<String, String> result = Arrays.stream(str)
.filter(map::containsKey)
.collect(Collectors.toMap(e -> e, map::get));
And if you have duplicate elements in the array:
Map<String, String> result = Arrays.stream(str)
.filter(map::containsKey)
.collect(Collectors.toMap(e -> e, map::get, (o, n) -> o));
The last parameter (o, n) -> o is a merge function that is applied to values when there are duplicates keys on the map. In this case, the merge function keeps the old value and discards the new one.

Sorting map based on list values

I have a map like below:
Map<String, String> map1 = new HashMap<String, String>();
and the contents are:
ID_1 -> ID_2
------------
100 -> 10
200 -> 20
300 -> 30
Based on the value of ID_2 I have to query an oracle table and get a code value corresponding to each entry:
ID_1 -> ID_2 -> code
--------------------
100 -> 10 -> 8
200 -> 20 -> 2
300 -> 30 -> 9
and then I will have to get the map1 sorted in ascending way by the code value i.e the result should be:
200 -> 20
100 -> 10
300 -> 30
I have thought of creating an intermediary map with <ID_1, List<ID_2,code>> as <K,V> and then sort using the code value and then get the final output.
Is there any shorter way to do so, like without using an intermediary map?
You try this code below: I used int[] array instead of List
public class Test {
public static void main(String[] args) throws Exception {
Map<String, int[]> map = new HashMap<>();
map.put("100", new int[]{10, 8});
map.put("200", new int[]{20, 2});
map.put("300", new int[]{30, 9});
Map<String, int[]> sortByValue = sortByValue(map);
for (Map.Entry<String, int[]> entry : sortByValue.entrySet()) {
System.out.println(entry.getKey() +" -> "+ entry.getValue()[0]);
}
}
private static Map<String, int[]> sortByValue( Map<String, int[]> map ) {
List<Map.Entry<String, int[]>> list = new LinkedList<>(map.entrySet());
Collections.sort(list, (o1, o2) -> Integer.compare(o1.getValue()[1], o2.getValue()[1]));
Map<String, int[]> result = new LinkedHashMap<>();
for (Map.Entry<String, int[]> entry : list) {
result.put(entry.getKey(), entry.getValue());
}
return result;
}
}
And it is the result:
200 -> 20
100 -> 10
300 -> 30
Based on map1 you can build new map:
Map<String, Pair<String, String> map2
where key is id from oracle db.
As you need to have ordering you can use TreeMap and method
Map.Entry<K,V> firstEntry();
I would express you logic as follow :
Get all entries in the map
Affect to each one its score (through the database call)
Order the entries in a final map according to step 2
It is important to notice that few maps have ordering constraints. The base implementation that comes to mind is LinkedHashMap. Furthermore "reordering an existing map" seems like a strange idea that is not backed by any methods in the Map interface. So in a nutshell, saying you need to return a Map that has an ordering constraint seems like a bad/incomplete idea - but it is certainly doable.
I would also adivse against using a TreeMap which is a Map ordered by a Comparator because I see no constraint that your ordering values are unique. Plus, your ordering is on the values, not the keys, of the map. Such a comparator would not be straightforward at all.
So, in short, what I would do is
LinkedHashMap<String, String> sorted = map.entrySet().stream()
// This is your DB call
.sorted(Comparator.comparing(entry -> getDBValue(entry)))
// Now we have an ordered stream of key/value entries from the original map
.collect(
// We flush back to a Map
Collectors.toMap(
// Keeping the original keys as is
Map.Entry::getKey,
// Keeping the original values as is
Map.Entry::getValue,
// This merge step should never be called, as keys are unique. Maybe you could assert that and fail if this is called
(v1, v2) -> v1,
// This is where you instanciate the Map that respects ordering of keys. Here, LinkedHashMap is iterable in the order of insertion, which is what we want.
LinkedHashMap::new
)
);
With Java streams you can achieve this without using any additional collections, here is an implementation.
To maintain order have used LinkedHashMap in the collector
For simplicity I have taken one more map to hold the db values [you need to change this to get from DB]
Map<String, String> map1 = new HashMap<String, String>();
map1.put("100", "10");
map1.put("200", "20");
map1.put("300", "30");
Map<String, String> dbmap = new HashMap<String, String>();
dbmap.put("10", "8");
dbmap.put("20", "2");
dbmap.put("30", "9");
Comparator<String> comp = (k1, k2) -> dbmap.get(map1.get(k1)).compareTo(dbmap.get(map1.get(k2)));
Map<String, String> queryMap = map1.keySet().stream().sorted(comp)
.collect(toMap((String key) -> key, value -> (String) map1.get(value), (u, v) -> {
throw new IllegalStateException(String.format("Duplicate key %s", u));
}, LinkedHashMap::new));
System.out.println(queryMap);
Ouput
{200=20, 100=10, 300=30}

Create a map from a list of maps

I have a list of maps.
List<Map<Integer, String>>
The values in the list are, for example
<1, String1>
<2, String2>
<1, String3>
<2, String4>
As an end result, I want a Map>, like
<1, <String1, String3>>
<2, <String2, String4>>
How can I achieve this in Java.
CODE :
List<Map<Integer, String>> genericList = new ArrayList<Map<Integer,String>>();
for(TrackActivity activity : activityMajor){
Map<Integer, String> mapIdResponse = activity.getMapIdResponse();
genericList.add(mapIdResponse);
}
Now this genericList is the input and from this list, based on the same ids I want a
Map<Integer, List<String>> mapIdResponseList
Basically, to club the responses which are String based on the ids, grouping the responses with same id in a list and then creating a new map with that id as the key and the list as its value.
You can do it the following with Java 8:
private void init() {
List<Map<Integer, String>> mapList = new ArrayList<>();
Map<Integer, String> map1 = new HashMap<>();
map1.put(1, "String1");
mapList.add(map1);
Map<Integer, String> map2 = new HashMap<>();
map2.put(2, "String2");
mapList.add(map2);
Map<Integer, String> map3 = new HashMap<>();
map3.put(1, "String3");
mapList.add(map3);
Map<Integer, String> map4 = new HashMap<>();
map4.put(2, "String4");
mapList.add(map4);
Map<Integer, List<String>> response = mapList.stream()
.flatMap(map -> map.entrySet().stream())
.collect(
Collectors.groupingBy(
Map.Entry::getKey,
Collectors.mapping(
Map.Entry::getValue,
Collectors.toList()
)
)
);
response.forEach((i, l) -> {
System.out.println("Integer: " + i + " / List: " + l);
});
}
This will print:
Integer: 1 / List: [String1, String3]
Integer: 2 / List: [String2, String4]
Explanation (heavily warranted), I am afraid I cannot explain every single detail, you need to understand the basics of the Stream and Collectors API introduced in Java 8 first:
Obtain a Stream<Map<Integer, String>> from the mapList.
Apply the flatMap operator, which roughly maps a stream into an already existing stream.
Here: I convert all Map<Integer, String> to Stream<Map.Entry<Integer, String>> and add them to the existing stream, thus now it is also of type Stream<Map.Entry<Integer, String>>.
I intend to collect the Stream<Map.Entry<Integer, String>> into a Map<Integer, List<String>>.
For this I will use a Collectors.groupingBy, which produces a Map<K, List<V>> based on a grouping function, a Function that maps the Map.Entry<Integer, String> to an Integer in this case.
For this I use a method reference, which exactly does what I want, namely Map.Entry::getKey, it operates on a Map.Entry and returns an Integer.
At this point I would have had a Map<Integer, List<Map.Entry<Integer, String>>> if I had not done any extra processing.
To ensure that I get the correct signature, I must add a downstream to the Collectors.groupingBy, which has to provide a collector.
For this downstream I use a collector that maps my Map.Entry entries to their String values via the reference Map.Entry::getValue.
I also need to specify how they are being collected, which is just a Collectors.toList() here, as I want to add them to a list.
And this is how we get a Map<Integer, List,String>>.
Have a look at guavas MultiMap. Should be exactly what you are looking for:
http://code.google.com/p/guava-libraries/wiki/NewCollectionTypesExplained#Multimap

Categories