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
Related
I have a List<Map<String,String>>
such as:
Map<String, String> m1 = new HashMap<>();
m1.put("date", "2020.1.5");
m1.put("B", "10");
Map<String, String> m2 = new HashMap<>();
m2.put("date", "2020.1.5");
m2.put("A", "20");
Map<String, String> m3 = new HashMap<>();
m3.put("date", "2020.1.6");
m3.put("A", "30");
Map<String, String> m4 = new HashMap<>();
m4.put("date", "2020.1.7");
m4.put("C", "30");
List<Map<String, String>> before = new ArrayList<>();
before.add(m1);
before.add(m2);
before.add(m3);
before.add(m4);
My expect result is to generate a new List map, which is grouped by date , and all the entry set in the same date would be put together, like:
[{"A":"20","B":"10","date":"2020.1.5"},{"A":"30","date":"2020.1.6"},{"C":"30","date":"2020.1.7"}]
I tried with the following method, but always not my expect result.
stream().flatmap().collect(Collectors.groupingBy())
Some Additional Comments for this problem:
I worked this out with for LOOP, but the application hangs when the list size is about 50000, so I seek a better performant way to do this. Java 8 stream flat map is a perhaps way as far as I know.
So the key point is not only to remap this but also with the most performant way to do this.
before
.stream()
.collect(Collectors.toMap((m) -> m.get("date"), m -> m, (a,b) -> {
Map<String, String> res = new HashMap<>();
res.putAll(a);
res.putAll(b);
return res;
}))
.values();
This is the solution you're looking for.
The toMap function receives 3 parameters:
the key mapper, which in your case is the date
the value mapper, which is the map itself that's being processed
the merge function, which takes 2 maps with the same date and puts all the keys together
Output:
[{date=2020.1.5, A=20, B=10}, {date=2020.1.6, A=30}, {date=2020.1.7, C=30}]
You can do this way using groupingBy and Collector.of
List<Map<String, String>> list = new ArrayList<>(before.stream()
.collect(Collectors.groupingBy(
k -> k.get("date"),
Collector.of( HashMap<String,String>::new,
(m,e)-> m.putAll(e),
(map1,map2)->{ map1.putAll(map2); return map1;}
))).values());
Here, first use Collectors.groupingBy to group by date. Then define custom collector using Collector.of to collect List<Map<String, String>> into Map<String, String>. After create list using map values.
And using Collectors.flatMapping from Java 9
List<Map<String, String>> list = new ArrayList<>(before.stream()
.collect(Collectors.groupingBy(
k -> k.get("date"),
Collectors.flatMapping(m -> m.entrySet().stream(),
Collectors.toMap(k -> k.getKey(), v -> v.getValue(), (a,b) -> a))))
.values());
You can achieve the very same result using a certain number of Collectors, orderly:
Collectors.groupingBy to group by the date
Collectors.reducing to merge the Map<String, String> items
Collectors.collectingAndThen to transform the values from Map<String, Optional<Map<String, String>>>, as a result of the previous reducing to the final output List<Map<String, String>>.
List<Map<String, String>> list = before.stream()
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(
m -> m.get("date"),
Collectors.reducing((l, r) -> {
l.putAll(r);
return l; })
),
o -> o.values().stream()
.flatMap(Optional::stream)
.collect(Collectors.toList())));
The list contains what are you looking for:
[{date=2020.1.5, A=20, B=10}, {date=2020.1.6, A=30}, {date=2020.1.7, C=30}]
Important: This solution has two he disadvantages:
It looks clumsy and might not be clear for an independent viewer
It mutates (modifies) the original maps included in the List<Map<String, String>> before.
It can be done as follows:
List<Map<String, String>> remapped = before.stream()
.collect(Collectors.groupingBy(m -> m.get("date")))
.values().stream()
.map(e -> e.stream()
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (x1, x2) -> x1)))
.collect(Collectors.toList());
remapped.forEach(System.out::println);
Output:
{date=2020.1.5, A=20, B=10}
{date=2020.1.6, A=30}
{date=2020.1.7, C=30}
Given:
enum Food{
FRUITS, VEGGIES;
}
Map<String, List<String>> basketMap = new HashMap<>();
basketMap .put("bucket1", Arrays.asList("apple", "banana"));
basketMap .put("bucket2", Arrays.asList("orange", "kiwi"));
basketMap .put("bucket3", Arrays.asList("banana", "orange"));
Need to generate a Map of map of list(populte fruitBaskerMap)
Map<String, Map<Food, List<String>> fruitBasketMap = new HashMap<>();
Final output:
fruitBasketMap:
[
bucket1, [Food.FRUITS, {"apple", "banana"}],
bucket2, [Food.FRUITS, {"orange", "kiwi"}],
bucket3, [Food.FRUITS, {"banana", "orange"}]
]
I tried the below (but was not successful)
fruitBasketMap = basketMap.entrySet().stream().collect(
Collectors.toMap(Map.Entry::getKey,
Collectors.toMap(Food.FRUITS,
Collectors.toList(Map.Entry::getValue())
)
)
);
Can somebody let me know how do I do that?
This implementation seems to be working (using Java 9 Map.of):
Map<String, Map<Food, List<String>>> fruitBasketMap = basketMap.entrySet().stream()
.map(e -> Map.entry(e.getKey(), Map.of(Food.FRUITS, e.getValue())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Output of the test:
{
bucket2={FRUITS=[orange, kiwi]},
bucket3={FRUITS=[banana, orange]},
bucket1={FRUITS=[apple, banana]}
}
This should do the trick in Java 8+ way:
fruitBasketMap =
basketMap.entrySet()
.stream()
.collect(groupingBy(Entry::getKey,
toMap(e -> Food.FRUITS, Entry::getValue)));
The collector above works in the following way:
Groups the elements by their keys, thus the keys remain the same as in the original map.
Collects the entries into a new map with
key: Food.FRUITS (constant for all entries)
value: the original value (list of strings)
This would work with Java 8
Map<String, Map<Food, List<String>>> collect = basketMap.entrySet().stream().collect(
Collectors.toMap(basketMapEntry -> basketMapEntry.getKey(),
basketMapEntry -> basketMapEntry.getValue().stream()
.collect(Collectors.groupingBy(
item -> type.get(item)
)
)
)
);
I am using the groupingBy collector to group the list of items based on the enum.
Assumption is that there would a Map to know the value of Food based on the entry. Something like:
Map<String, Food> type = new HashMap<>();
type.put("apple", Food.FRUITS);
type.put("banana", Food.FRUITS);
type.put("orange", Food.FRUITS);
type.put("kiwi", Food.FRUITS);
type.put("kiwi", Food.FRUITS);
type.put("potato", Food.VEGGIES);
type.put("carrot", Food.VEGGIES);
You can achieve this in Java-8 way itself. Try the below one. Since the inner map will always have single entry, you can use singletonMap
Map<String, Map<Food, List<String>>> result = basketMap.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> Collections.singletonMap(Food.FRUITS, entry.getValue())));
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.
I have a collection of maps:
Collection<Map<String,Double>> myCol = table.values();
I would like to transform this into a Map
Map<String, Double>
such that, for a matching key, values are summed up. Using a for loop, it is rather simple:
Map<String, Double> outMap = new HashMap<>();
for (Map<String, Double> map : myCol) {
outMap = mergeMaps(outMap, map);
}
and mergeMaps() is defined as
mergeMaps(Map<String, Double> m1, Map<String, Double> m2){
Map<String, Double> outMap = new TreeMap<>(m1);
m2.forEach((k,v) -> outMap.merge(k,v,Double::sum)); /*sum values if key exists*/
return outMap;
}
However, I would like to use streams to get a map from collection. I have tried as follows:
Map<String, Double> outMap = new HashMap<>();
myCol.stream().forEach(e-> outMap.putAll(mergeMaps(outMap,e)));
return outMap;
This works without a problem. However, can I still improve it? I mean, how can I use collectors in it?
From your input, you can fetch the stream of maps and then flatmap it to have a Stream<Map.Entry<String, Double>>. From there, you collect them into a new map, specifying that you want to sum the values mapped to the same key.
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.summingDouble;
import static java.util.stream.Collectors.toMap;
....
Map<String, Double> outMap =
myCol.stream()
.flatMap(m -> m.entrySet().stream())
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue, Double::sum));
Alternatively, you can use groupingBy instead of toMap:
.collect(groupingBy(Map.Entry::getKey, summingDouble(Map.Entry::getValue)));
myCol.stream()
.flatMap(x -> x.entrySet().stream())
.collect(Collectors.groupingBy(
Entry::getKey,
TreeMap::new,
Collectors.summingDouble(Entry::getValue)));
Well, the other proposed solutions show that a pure stream solution is short, but if you wanted to use your existing mergeFunction (because in other cases it is more complex for example), you could just hand it over to Stream.reduce:
Optional<Map<String, Double>> outMap = myCol.stream().reduce((m1, m2) -> mergeMaps(m1, m2));
Your initial approach with the forEach is pretty much a streamyfied for loop and violates the concept of functions having no side effects. The reduce (or the above collects) handles all the data merging internally, without changing the input collection.
With streams:
Map<String, Double> outMap = myCol.stream()
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey, // key of the result map
Map.Entry::getValue, // value of the result map
Double::sum, // how to merge values for equal keys
TreeMap::new)); // the type of map to be created
This uses Collectors.toMap to create the result TreeMap.
You can do it without streams, though. I think your version is a little bit complicated, you could refactor it as follows:
Map<String, Double> outMap = TreeMap<>();
myCol.forEach(map -> map.forEach((k, v) -> outMap.merge(k, v, Double::sum)));
Which is shorter, easy and most readable.
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}