I have model Person[city, name]. I have collected them in Map And Grouped them by city. I need to trace the city that has most no of person staying there and return only that entry as part of Map. I'v tried and also it is working but i was wondering is there any better way of doing.
Comparator<Entry<String, List<Person>>> compareByCityPopulation =
Comparator.comparing(Entry<String, List<Person>>::getValue, (s1, s2) -> {
return s1.size() - s2.size();
});
HashMap mapOfMostPopulatedCity = persons.stream()
.collect(Collectors.collectingAndThen(Collectors.groupingBy(Person::getCity), m -> {
Entry<String, List<Person>> found = m.entrySet().stream().max(compareByCityPopulation).get();
HashMap<String, List<Person>> hMap = new HashMap<>();
hMap.put(found.getKey(), found.getValue());
return hMap;
}));
System.out.println("*City with Most no of people*");
mapOfMostPopulatedCity.forEach((place, peopleDetail) -> System.out.println("Places " + place + "-people detail-" + peopleDetail));
Please Suggest how can we write better in java 8.
After getting the max map entry, you have to convert that into a map which has a single entry. For that you can use Collections.singletonMap()
Map<String, List<Person>> mapOfMostPopulatedCity = persons.stream()
.collect(Collectors.groupingBy(Person::getCity)).entrySet().stream()
.max(Comparator.comparingInt(e -> e.getValue().size()))
.map(e -> Collections.singletonMap(e.getKey(), e.getValue()))
.orElseThrow(IllegalArgumentException::new);
With Java9 you can use Map.of(e.getKey(), e.getValue()) to build the map with a single entry.
Suppose if you have an list of persons
List<Person> persons = new ArrayList<Person>();
Then first group By them based on city and then get the Entry with max values in list max will return Optional of Entry, so i won't make it complicate i will just use HashMap to store the result if it present in optional or else will return the empty Map
Map<String, List<Person>> resultMap = new HashMap<>();
persons.stream()
.collect(Collectors.groupingBy(Person::getCity)) //group by city gives Map<String,List<Person>>
.entrySet()
.stream()
.max(Comparator.comparingInt(value->value.getValue().size())) // return the Optional<Entry<String, List<Person>>>
.ifPresent(entry->resultMap.put(entry.getKey(),entry.getValue()));
//finally return resultMap
Related
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
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 9 months ago.
Improve this question
What I have is:
Map<String, String> map = new HashMap<>();
map.put("Shop1", "Product1");
map.put("Shop2", "Product2");
map.put("Shop3", "Product1");
map.put("Shop4", "Product2");
map.put("Shop5", "Product3");
What I want is:
Map<String, List<String>> result = new HashMap<>();
Wherein result contains:
Product1 -> Shop1,Shop3
Product2 -> Shop2,Shop4
Here Product1 is found at multiple times in shops Shop1 & Shop3 and Product2 is found multiple times in shops Shop2 & Shop4.
What you're trying to do is to invert the map (values become keys and keys get grouped by old values). There should be libraries that do that, but a sample solution with streams:
result = map.entrySet()
.stream()
.filter(e -> Collections.frequency(map.values(), e.getValue()) > 1)
.collect(Collectors.groupingBy(Map.Entry::getValue, Collectors.mapping(Map.Entry::getKey, Collectors.toList())))
You can do this with Guava's Multimap.
final Map<String, String> map = new HashMap<>();
map.put("Shop1", "Product1");
map.put("Shop2", "Product2");
map.put("Shop3", "Product1");
map.put("Shop4", "Product2");
map.put("Shop5", "Product3");
final Multimap<String, String> shopToProduct = Multimaps.forMap(map), productToShop = HashMultimap.create();
Multimaps.invertFrom(shopToProduct, productToShop);
// Prints the output you want
productToShop.asMap().entrySet().forEach(System.out::println);
// You can also get the backing map, but ideally use the Multimap abstraction
final Map<String, Collection<String>> backingMap = productToShop.asMap();
Find the distinct values first. After that, you can iterate through each value and search for their key on the map, whenever you find a key you just put them into a list.
List<String> list = map.values().stream().distinct().collect(Collectors.toList()); //Collecting the distinct values
for (String value : list) { //iterating through each key
List<String> temp = map.entrySet()
.stream()
.filter(entry -> value.equals(entry.getValue())) //filtering out similar entries
.map(Map.Entry::getKey)
.collect(Collectors.toList());
result.put(value, temp);
}
System.out.println(result);
You can use Guava's Multimap to achieve this, however you'd need to invert the map.
Multimap<String, String> map = HashMultimap.create();
map.put("Product1", "Shop1");
map.put("Product2", "Shop2");
map.put("Product2", "Shop1");
map.put("Product2", "Shop4");
map.put("Product3", "Shop5");
System.out.println(map.get("Product2")); // [Shop4, Shop1, Shop2]
Alternatively, if you'd prefer not to invert the original map - you can use a Map<String, List<String> (Or combine the following solution with the Multimap above) and add records with computeIfAbsent, then invert the map to get the values you want.
Map<String, List<String>> map = new HashMap<>();
map.computeIfAbsent("Shop1", (x) -> new ArrayList<>()).add("Product1");
map.computeIfAbsent("Shop2", (x) -> new ArrayList<>()).add("Product2");
map.computeIfAbsent("Shop3", (x) -> new ArrayList<>()).add("Product1");
map.computeIfAbsent("Shop4", (x) -> new ArrayList<>()).add("Product2");
map.computeIfAbsent("Shop5", (x) -> new ArrayList<>()).add("Product3");
// Invert the map
Map<String, List<String>> invertedMap = map
.entrySet()
.stream()
.collect(HashMap::new, (m, entry) -> {
entry.getValue().forEach((value) -> {
m.computeIfAbsent(value, (x) -> new ArrayList<>()).add(entry.getKey());
});
}, Map::putAll);
System.out.println(invertedMap); // {Product3=[Shop5], Product1=[Shop1, Shop3], Product2=[Shop4, Shop2]}
Map<String, Map<String, ArrayList<Employee>>> map = new HashMap<String, Map<String, ArrayList<Employee>>>();
ArrayList<Employee> list = new ArrayList<Employee>();
Map<String, ArrayList<Employee>> empMap = new HashMap<>();
Employee e1 = new Employee("Raju", 10000);
Employee e2 = new Employee("Naresh", 2000);
Employee e3 = new Employee("Sugg", 30000);
Employee e4 = new Employee("Asd", 30000);
list.add(e1);
list.add(e2);
list.add(e3);
list.add(e4);
empMap.put("w1", list);
map.put("t1", empMap);
Comparator<Employee> cmp = (i1, i2) -> i1.getSal().compareTo(i2.getSal());
Comparator<Employee> cmp1 = (i1, i2) -> i1.getName().compareTo(i2.getName());
map.values().stream().map(m -> {
return m.values().stream().map(l -> {
l.sort(cmp.thenComparing(cmp1));
return l;
});
}).flatMap(m -> m).forEach(l -> {
l.forEach(l1 -> System.out.println(l1.getName() + " " + l1.getSal()));
});
Map is the main object, empMap is the map inside main map, list is the list of Employee objets.
I tried the sorting in above but Is any other way above code can be optimized?
If you want to just sort the ArrayList<Employee> inside the Map you can use forEach to iterate the map and use sort method on ArrayList
map.forEach((key,value)->{
value.forEach((nKey,nValue)->{
nValue.sort(Comparator.comparing(Employee::getSalary).thenComparing(Employee::getName));
});
});
You have been created two Comparator functions - it is good way and practice, in additional you can change last part of your code - it look slightly confusing:
map.values().stream()
.flatMap(m -> m.values().stream())
.forEach(l -> l.sort(cmp.thenComparing(cmp1)));
Or
map.forEach((key,value)->
value.values()
.forEach(v -> v.sort(cmp.thenComparing(cmp1))));
If you want a complete streams solution you can do it as follows. This just streams each outer out and inner in entrySets and the sorts the list based on the specified requirements. I added a duplicate name with a different salary for a complete demonstration.
Map<String, Map<String, List<Employee>>> results = map
.entrySet().stream()
.collect(Collectors.toMap(out -> out.getKey(),
out -> out.getValue().entrySet().stream()
.collect(Collectors.toMap(
in -> in.getKey(),
in -> in.getValue().stream()
.sorted(Comparator
.comparing(
Employee::getName)
.thenComparing(
Employee::getSalary))
.collect(Collectors
.toList())))));
results.entrySet().forEach(System.out::println);
Prints
t1={w1=[[Asd, 30000], [Naresh, 2000], [Naresh, 10000], [Raju, 10000], [Sugg,
30000]]}
Of course, since the List itself is mutable it is much more straight forward to just sort the list in place as described in other answer(s) and not create a new map.
Here I am posting sample datastructure
I have a list List<Result> resultsList;
class Result {
String name;
Map<String,Integer> resultMap;
}
Now I would like to stream through this list and get the map.
resultList.stream().filter(result->"xxx".equals(result.getName()))
.map(result->result.getResultMap);
It returns Stream<Map<String,Integer>> but I need only Map<String,Integer>.
How to get it using java 8 streams?
Update:
As geneqew mentioned
This is how my datastructure looks
List<Result> resultsList;
Map<String, Integer> map1 = new HashMap<>();
map1.put("m1", 1);
Map<String, Integer> map2 = new HashMap<>();
map2.put("m2", 2);
Map<String, Integer> map3 = new HashMap<>();
map3.put("m3", 3);
results = Arrays.asList(
new Result("r1", map1),
new Result("r2", map2),
new Result("r3", map3)
);
I would like to retrieve single map based on name.
for (Result result: resultsList)
{
if ('xxx'.equals(result.getName())
{
return result.getResultMap();
}
}
Since you want to return the result map of the first Result element to pass your filter, you can obtain it with findFirst():
Optional<Map<String,Integer>> resultMap =
resultList.stream()
.filter(result->"xxx".equals(result.getName()))
.map(Result::getResultMap)
.findFirst();
You can extract the Map from the Optional this way:
Map<String,Integer> resultMap =
resultList.stream()
.filter(result->"xxx".equals(result.getName()))
.map(Result::getResultMap)
.findFirst()
.orElse(null);
if you're only looking for one item:
resultList.stream()
.filter(result -> "xxx".equals(result.getName()))
.map(Result::getResultMap)
.findAny();
if the filter could match more than one item then you'll need to flatten then toMap it:
resultList.stream()
.filter(result-> "xxx".equals(result.getName()))
.flatMap(result -> result.getResultMap().entrySet().stream())
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
if there can be duplicates then use the merge function to resolve collisions:
resultList.stream()
.filter(result -> "xxx".equals(result.getName()))
.flatMap(result -> result.getResultMap().entrySet().stream())
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (l, r) -> l));
Since you only wanted the map that matches the results' name then:
results.stream()
.filter(r-> r.getName().equals("r2"))
.map(r-> r.getResultMap())
.findFirst()
.orElse(null);
given you have a sample content of:
List<Result> results;
Map<String, Integer> map1 = new HashMap<>();
map1.put("m1", 1);
Map<String, Integer> map2 = new HashMap<>();
map2.put("m2", 2);
Map<String, Integer> map3 = new HashMap<>();
map3.put("m3", 3);
results = Arrays.asList(
new Result("r1", map1),
new Result("r2", map2),
new Result("r3", map3)
);
A bit of explanation, you got a stream because the last operation in your stream is a map; assuming in your list its possible to have more than 1 result with the same name, findFirst will return the first match if found otherwise an empty optional is returned; Finally orElse to get terminate the stream, providing a null value on empty match.
So I want to explain why you receive stream and not a map. The reason of this is because in the beginning you have List with Result objects that you filter by some criteria (in your case "xxx".equals(result.getName())).
Now you can have as result zero, one or more elements that will pass this criteria! Java does not know how many elements will pass at compile time and that is why you get Stream.
Imagine situation that you have two Result objects that have the same name 'xxx' then you will have two maps. The question is what you want to do? If you get only one of the maps you will loose information. If you want to get all of them, please try something like this:
List<Map<String,Integer>> listWithResultMaps = resultList.stream()
.filter(result->"xxx".equals(result.getName()))
.map(result->result.getResultMap())
.collect(Collectors.toList());
Now in this listWithResultMaps you can process all maps that you have as result of your filter.
Good Luck!
I have the following data:
List<Map<String, Object>> products = new ArrayList<>();
Map<String, Object> product1 = new HashMap<>();
product1.put("Id", 1);
product1.put("number", "123");
product1.put("location", "ny");
Map<String, Object> product2 = new HashMap<>();
product2.put("Id", 1);
product2.put("number", "456");
product2.put("location", "ny");
Map<String, Object> product3 = new HashMap<>();
product3.put("Id", 2);
product3.put("number", "789");
product3.put("location", "ny");
products.add(product1);
products.add(product2);
products.add(product3);
I'm trying to stream over the products list, group by the id and for each id have a list on number, while returning a Map that contains three keys: Id, List of number, and a location.
So my output would be:
List<Map<String, Object>>> groupedProducts
map[0]
{id:1, number[123,456], location:ny}
map[1]
{id:2, number[789], location:ny}
I have tried:
Map<String, List<Object>> groupedProducts = products.stream()
.flatMap(m -> m.entrySet().stream())
.collect(groupingBy(Entry::getKey, mapping(Entry::getValue, toList())));
which prints:
{number=[123, 456, 789], location=[ny, ny, ny], Id=[1, 1, 2]}
I realise Map<String, List<Object>> is incorrect, but it's the best I could achieve to get the stream to work. Any feedback is appreciated.
In your case grouping by Id key with Collectors.collectingAndThen(downstream, finisher) could do the trick. Consider following example:
Collection<Map<String, Object>> finalMaps = products.stream()
.collect(groupingBy(it -> it.get("Id"), Collectors.collectingAndThen(
Collectors.toList(),
maps -> (Map<String, Object>) maps.stream()
.reduce(new HashMap<>(), (result, map) -> {
final List<Object> numbers = (List<Object>) result.getOrDefault("number", new ArrayList<>());
result.put("Id", map.getOrDefault("Id", result.getOrDefault("Id", null)));
result.put("location", map.getOrDefault("location", result.getOrDefault("location", null)));
if (map.containsKey("number")) {
numbers.add(map.get("number"));
}
result.put("number", numbers);
return result;
}))
)
)
.values();
System.out.println(finalMaps);
In the first step you group all maps with the same Id value to a List<Map<String,Object>> (this is what Collectors.toList() passed to .collectingAndThen() does). After creating that list "finisher" function is called - in this case we transform list of maps into a single map using Stream.reduce() operation - we start with an empty HashMap<String,Object> and we iterate over maps, take values from current map in iteration and we set values according to your specification ("Id" and "location" gets overridden, "number" keeps a list of values).
Output
[{number=[123, 456], location=ny, Id=1}, {number=[789], location=ny, Id=2}]
To make code more simple you can extract BiOperator passed to Stream.reduce to a method and use method reference instead. This function defines what does it mean to combine two maps into single one, so it is the core logic of the whole reduction.