Java - order a collections by number of objects in each list? - java

Collection<List<Person>> personsByDepartment=
persons.stream()
.collect(Collectors.groupingBy(Person::getDepartment))
.values();
I have the following Collection above that groups a list of People into lists based off their department. This is working as expected.
How can I ensure this list is sorted so that the list with most amount of people in it is first?

Provide a Comparator to compare the lists by their size. You could stream the values again to sort:
Collection<List<Person>> personsByDepartment = persons.stream()
.collect(Collectors.groupingBy(Person::getDepartment))
.values()
.stream()
.sorted((l1, l2) -> Integer.compare(l2.size(), l1.size()))
.collect(Collectors.toList());
Edit: As Michael pointed out in comments the same comparator can be written as:
Comparator.<List<Person>>comparingInt(List::size).reversed()
It's more obvious, that the ordering is being reversed.

Related

Filter a list with streams based on ids from another list

I have a list of Product objects with 30 objects.
I have created a fix list with strings of ids.
List<String> productsIdsForFreeSchoolYear = Arrays.asList("169", "172", "198", "213", "358", "4529", "6602", "5958");
What I want to do is to get list of Product only contains the ids from productsIdsForFreeSchoolYear list.
This is what i tried, but its seems its only check if the ids exists? Im i right?
productsSelectable.stream()
.distinct()
.filter(productsIdsForFreeSchoolYear::contains)
.collect(Collectors.toList());
productsSelectable is the list of Product.
Thank you!
In your code, contains() will always return false because it's a list of IDs (strings), and you're passing it a Product instance. You probably want something like this:
productsSelectable.stream()
.distinct()
.filter(p -> productsIdsForFreeSchoolYear.contains(p.getId()))
.collect(Collectors.toList());

Collect results of a map operation in a Map using Collectors.toMap or groupingBy

I've got a list of type List<A> and with map operation getting a collective list of type List<B> for all A elements merged in one list.
List<A> listofA = [A1, A2, A3, A4, A5, ...]
List<B> listofB = listofA.stream()
.map(a -> repo.getListofB(a))
.flatMap(Collection::stream)
.collect(Collectors.toList());
without flatmap
List<List<B>> listOflistofB = listofA.stream()
.map(a -> repo.getListofB(a))
.collect(Collectors.toList());
I want to collect the results as a map of type Map<A, List<B>> and so far trying with various Collectors.toMap or Collectors.groupingBy options but not able to get the desired result.
You can use the toMap collector with a bounded method reference to get what you need. Also notice that this solution assumes you don't have repeated A instances in your source container. If that precondition holds this solution would give you the desired result. Here's how it looks.
Map<A, Collection<B>> resultMap = listofA.stream()
.collect(Collectors.toMap(Function.identity(), repo::getListofB);
If you have duplicate A elements, then you have to use this merge function in addition to what is given above. The merge function deals with key conflicts if any.
Map<A, Collection<B>> resultMap = listofA.stream()
.collect(Collectors.toMap(Function.identity(), repo::getListofB,
(a, b) -> {
a.addAll(b);
return a;
}));
And here's a much more succinct Java9 approach which uses the flatMapping collector to handle repeated A elements.
Map<A, List<B>> aToBmap = listofA.stream()
.collect(Collectors.groupingBy(Function.identity(),
Collectors.flatMapping(a -> getListofB(a).stream(),
Collectors.toList())));
It would be straight forward,
listofA.stream().collect(toMap(Function.identity(), a -> getListofB(a)));
In this answer, I'm showing what happens if you have repeated A elements in your List<A> listofA list.
Actually, if there were duplicates in listofA, the following code would throw an IllegalStateException:
Map<A, Collection<B>> resultMap = listofA.stream()
.collect(Collectors.toMap(
Function.identity(),
repo::getListofB);
The exception might be thrown because Collectors.toMap doesn't know how to merge values when there is a collision in the keys (i.e. when the key mapper function returns duplicates, as it would be the case for Function.identity() if there were repeated elements in the listofA list).
This is clearly stated in the docs:
If the mapped keys contains duplicates (according to Object.equals(Object)), an IllegalStateException is thrown when the collection operation is performed. If the mapped keys may have duplicates, use toMap(Function, Function, BinaryOperator) instead.
The docs also give us the solution: in case there are repeated elements, we need to provide a way to merge values. Here's one such way:
Map<A, Collection<B>> resultMap = listofA.stream()
.collect(Collectors.toMap(
Function.identity(),
a -> new ArrayList<>(repo.getListofB(a)),
(left, right) -> {
left.addAll(right);
return left;
});
This uses the overloaded version of Collectors.toMap that accepts a merge function as its third argument. Within the merge function, Collection.addAll is being used to add the B elements of each repeated A element into a unqiue list for each A.
In the value mapper function, a new ArrayList is created, so that the original List<B> of each A is not mutated. Also, as we're creating an Arraylist, we know in advance that it can be mutated (i.e. we can add elements to it later, in case there are duplicates in listofA).
To collect a Map where keys are the A objects unchanged, and the values are the list of corresponding B objects, you can replace the toList() collector by the following collector :
toMap(Function.identity(), a -> repo.getListOfB(a))
The first argument defines how to compute the key from the original object : identity() takes the original object of the stream unchanged.
The second argument defines how the value is computed, so here it just consists of a call to your method that transforms a A to a list of B.
Since the repo method takes only one parameter, you can also improve clarity by replacing the lambda with a method reference :
toMap(Function.identity(), repo::getListOfB)

sort value of a map by comparator with java streams api

I have a Map and want to sort the values by a Comparator putting the result into a LinkedHashMap.
Map<String, User> sorted = test.getUsers()
.entrySet()
.stream()
.sorted(Map.Entry.comparingByValue(SORT_BY_NAME))
.collect(LinkedHashMap::new, (map, entry) -> map.put(entry.getKey(), entry.getValue()),
LinkedHashMap::putAll);
test.setUsers(sorted);
All works, however, I wonder if this can be simplified.
Actually I create a new Map and put that new Map into the setUsers(). Can I change the stream directly without creating a new LinkedHashMap?
With Collections.sort(Comparator), the list is directly sorted. However, Collections.sort does not work with Maps.
Why not use Collectors.toMap()?
.collect(Collectors.toMap(
Entry::getKey,
Entry::getValue,
(a, b) -> { throw new AssertionError("impossible"); },
LinkedHashMap::new));
You cannot perform in-place sorting on a collection that does not support array-style indexing, so for maps it is out of the question (except you use something like selection sort on a LinkedHashMap, but that would be a bad idea). Creating a new map is unavoidable. You can still simplify it though, by following shmosel's answer.

Java 8 HashMap<Integer, ArrayList<Integer>>

I am new Java 8 and want to sort a Map based on Key and then sort each list within values.
I tried to look for a Java 8 way to sort Keys and also value.
HashMap> map
map.entrySet().stream().sorted(Map.Entry.comparingByKey())
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue, (e1, e2) -> e2, LinkedHashMap::new));
I am able to sort the Map and I can collect each values within map to sort but is their a way we can do in java 8 and can both be combined.
To sort by key, you could use a TreeMap. To sort each list in the values, you could iterate over the values of the map by using the Map.values().forEach() methods and then sort each list by using List.sort. Putting it all together:
Map<Integer, List<Integer>> sortedByKey = new TreeMap<>(yourMap);
sortedByKey.values().forEach(list -> list.sort(null)); // null: natural order
This sorts each list in-place, meaning that the original lists are mutated.
If instead you want to create not only a new map, but also new lists for each value, you could do it as follows:
Map<Integer, List<Integer>> sortedByKey = new TreeMap<>(yourMap);
sortedByKey.replaceAll((k, originalList) -> {
List<Integer> newList = new ArrayList<>(originalList);
newList.sort(null); // null: natural order
return newList;
});
EDIT:
As suggested in the comments, you might want to change:
sortedByKey.values().forEach(list -> list.sort(null));
By either:
sortedByKey.values().forEach(Collections::sort);
Or:
sortedByKey.values().forEach(list -> list.sort(Comparator.naturalOrder()));
Either one of the two options above is much more expressive and shows the developer's intention in a better way than using null as the comparator argument to the List.sort method.
Same considerations apply for the approach in which the lists are not modified in-place.

List containing another list to set using lambda in java

I have a list containing Persons, each person has a list with his subjects inside.
I need to return a Set containing every subject using lambda, so far i've tried this:
list.stream().map(person -> person.getSubjects());
But that would get me a List> so i can't use it.
How could i print/get every string in the list of every person using lambdas?
Thanks.
list.stream().map(person -> person.getSubjects().stream()); is not a List, it's a Stream. If you want a Set, do this :
list.stream().flatMap(person -> person.getSubjects().stream()).collect(Collectors.toSet());
This will create an HashSet<Subject>. Note the use of flatMap instead of map to flatten the lists of subjects into a single stream. If you want another implementation of Set, for example TreeSet, do the following :
list.stream().flatMap(person -> person.getSubjects().stream())
.collect(Collectors.toCollection(TreeSet::new));
You can use flatMap:
Set<Subject> subjects = list.stream()
.map(person -> person.getSubjects())
.flatMap(subjects -> subjects.stream())
.collect(Collectors.toSet());
flatMap is good for "flattening" nested collections.

Categories

Resources