Sorting map based on list values - java

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}

Related

How do I generate a Map of Map of List (Map<String, Map<Enum, List<String>>>) in java using streams

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())));

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.

Sort map by key string (where key is actually an integer)

I am trying to sort a map to show in a dropdown. But I am not able to get any sorting done. This will return a new map. But not with the map sorted by the key as I would expect.
private Map<String, String> mapInstrumentIDs = new TreeMap<>();
Map<Object, Object> val = mapInstrumentIDs
.entrySet()
.stream()
.sorted(Map.Entry.comparingByKey())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
I of course didn't think about that the key is actually an integer. This means sorting it as a string does not give me the expected result (as integer sort). Changing the key to Integer and converting the value will yield the expected result.
By default a TreeMap guarantees that its elements will be sorted in ascending key order.
You should collect the results into a Map implementation that retains the order of its entries. LinkedHashMap will do:
Map<String, String> sorted = mapInstrumentIDs.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.collect(toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(x,y)-> {throw new AssertionError();},
LinkedHashMap::new
));
Normally one would make a copy as:
private SortedMap<String, String> mapInstrumentIDs = new TreeMap<>();
SortedMap<String, String> val = new TreeMap(mapInstrumentIDs);
If you want a copy with key type Comparable<?> and value type Object, you want to specify the initial map as TreeMap and cannot use the standard collectors:
SortedMap<Comparable, Object> val = mapInstrumentIDs
.entrySet()
.collect(TreeMap<Comparable, Object>::new,
(m, e) -> { m.put(e.getKey(), e.getValue()); return m; },
(m, m2) -> m.addAll(m2)
);

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

How to putAll on Java hashMap contents of one to another, but not replace existing keys and values?

I need to copy all keys and values from one A HashMap onto another one B, but not to replace existing keys and values.
Whats the best way to do that?
I was thinking instead iterating the keySet and checkig if it exist or not, I would
Map temp = new HashMap(); // generic later
temp.putAll(Amap);
A.clear();
A.putAll(Bmap);
A.putAll(temp);
It looks like you are willing to create a temporary Map, so I'd do it like this:
Map tmp = new HashMap(patch);
tmp.keySet().removeAll(target.keySet());
target.putAll(tmp);
Here, patch is the map that you are adding to the target map.
Thanks to Louis Wasserman, here's a version that takes advantage of the new methods in Java 8:
patch.forEach(target::putIfAbsent);
Using Guava's Maps class' utility methods to compute the difference of 2 maps you can do it in a single line, with a method signature which makes it more clear what you are trying to accomplish:
public static void main(final String[] args) {
// Create some maps
final Map<Integer, String> map1 = new HashMap<Integer, String>();
map1.put(1, "Hello");
map1.put(2, "There");
final Map<Integer, String> map2 = new HashMap<Integer, String>();
map2.put(2, "There");
map2.put(3, "is");
map2.put(4, "a");
map2.put(5, "bird");
// Add everything in map1 not in map2 to map2
map2.putAll(Maps.difference(map1, map2).entriesOnlyOnLeft());
}
Just iterate and add:
for(Map.Entry e : a.entrySet())
if(!b.containsKey(e.getKey())
b.put(e.getKey(), e.getValue());
Edit to add:
If you can make changes to a, you can also do:
a.putAll(b)
and a will have exactly what you need. (all the entries in b and all the entries in a that aren't in b)
You can make it in just 1 line if you change maps order in #erickson's solution:
mapWithNotSoImportantValues.putAll( mapWithImportantValues );
In this case you replace values in mapWithNotSoImportantValues with value from mapWithImportantValues with the same keys.
Java 8 solution using Map#merge
As of java-8 you can use Map#merge(K key, V value, BiFunction remappingFunction) which merges a value into the Map using remappingFunction in case the key is already found in the Map you want to put the pair into.
// using lambda
newMap.forEach((key, value) -> map.merge(key, value, (oldValue, newValue) -> oldValue));
// using for-loop
for (Map.Entry<Integer, String> entry: newMap.entrySet()) {
map.merge(entry.getKey(), entry.getValue(), (oldValue, newValue) -> oldValue);
}
The code iterates the newMap entries (key and value) and each one is merged into map through the method merge. The remappingFunction is triggered in case of duplicated key and in that case it says that the former (original) oldValue value will be used and not rewritten.
With this solution, you don't need a temporary Map.
Let's have an example of merging newMap entries into map and keeping the original values in case of the duplicated antry.
Map<Integer, String> newMap = new HashMap<>();
newMap.put(2, "EVIL VALUE"); // this will NOT be merged into
newMap.put(4, "four"); // this WILL be merged into
newMap.put(5, "five"); // this WILL be merged into
Map<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
newMap.forEach((k, v) -> map.merge(k, v, (oldValue, newValue) -> oldValue));
map.forEach((k, v) -> System.out.println(k + " " + v));
1 one
2 two
3 three
4 four
5 five
public class MyMap {
public static void main(String[] args) {
Map<String, String> map1 = new HashMap<String, String>();
map1.put("key1", "value1");
map1.put("key2", "value2");
map1.put("key3", "value3");
map1.put(null, null);
Map<String, String> map2 = new HashMap<String, String>();
map2.put("key4", "value4");
map2.put("key5", "value5");
map2.put("key6", "value6");
map2.put("key3", "replaced-value-of-key3-in-map2");
// used only if map1 can be changes/updates with the same keys present in map2.
map1.putAll(map2);
// use below if you are not supposed to modify the map1.
for (Map.Entry e : map2.entrySet())
if (!map1.containsKey(e.getKey()))
map1.put(e.getKey().toString(), e.getValue().toString());
System.out.println(map1);
}}
With Java 8 there is this API method to accomplish your requirement.
map.putIfAbsent(key, value)
If the specified key is not already associated with a value (or is mapped to null) associates it with the given value and returns null, else returns the current value.
As others have said, you can use putIfAbsent. Iterate over each entry in the map that you want to insert, and invoke this method on the original map:
mapToInsert.forEach(originalMap::putIfAbsent);

Categories