Java 8 List<T> into Map<K, V> - java

I want to convert List of Objects to Map, where Map's key and value located as attributes inside Object in List.
Here Java 7 snippet of such convertation:
private Map<String, Child> getChildren(List<Family> families ) {
Map<String, Child> convertedMap = new HashMap<String, Child>();
for (Family family : families) {
convertedMap.put(family.getId(), family.getParent().getChild());
}
return convertedMap;
}

It should be something similar to...
Map<String, Child> m = families.stream()
.collect(Collectors.toMap(Family::getId, f -> f.getParent().getChild()));

Jason gave a decent answer (+1) but I should point out that it has different semantics from the OP's Java 7 code. The issue concerns the behavior if two family instances in the input list have duplicate IDs. Maybe they're guaranteed unique, in which case there is no difference. If there are duplicates, though, with the OP's original code, a Family later in the list will overwrite the map entry for a Family earlier in the list that has the same ID.
With Jason's code (shown below, slightly modified):
Map<String, Child> getChildren(List<Family> families) {
return families.stream()
.collect(Collectors.toMap(Family::getId, f -> f.getParent().getChild()));
}
the Collectors.toMap operation will throw IllegalStateException if there are any duplicate keys. This is somewhat unpleasant, but at least it notifies you that there are duplicates instead of potentially losing data silently. The rule for Collectors.toMap(keyMapper, valueMapper) is that you need to be sure that the key mapper function returns a unique key for every element of the stream.
What you need to do about this -- if anything -- depends on the problem domain. One possibility is to use the three-arg version: Collectors.toMap(keyMapper, valueMapper, mergeFunction). This specifies an extra function that gets called in the case of duplicates. If you want to have later entries overwrite earlier ones (matching the original Java 7 code), you'd do this:
Map<String, Child> getChildren(List<Family> families) {
return families.stream()
.collect(Collectors.toMap(Family::getId, f -> f.getParent().getChild(),
(child1, child2) -> child2));
}
An alternative would be to build up a list of children for each family instead of having just one child. You could write a more complicated merging function that created a list for the first child and appended to this list for the second and subsequent children. This is so common that there is a special groupingBy collector that does this automatically. By itself this would produce a list of families grouped by ID. We don't want a list of families but instead we want a list of children, so we add a downstream mapping operation to map from family to child, and then collect the children into a list. The code would look like this:
Map<String, List<Child>> getChildren(List<Family> families) {
return families.stream()
.collect(Collectors.groupingBy(Family::getId,
Collectors.mapping(f -> f.getParent().getChild(),
Collectors.toList())));
}
Note that the return type has changed from Map<String, Child> to Map<String, List<Child>>.

Related

Java stream not putting all values of a List into a HashMap

I am working on a program where I need to take a list of objects and put them in a HashMap, using the class name as the key and the instance as the value.
I have the following code:
pets2.stream().forEach(pt -> animals.put(pt.getClass().getSimpleName(), pt));
Where pets2 is my list of animals is my HashMap. However, when I run this and then print the HashMap, only two objects from the list have been added into the Map.
Not even the first two, just two of them. Any idea as to what the issue is and how I can fix it?
If you have multiple instances of the same class in a list, you will lose information if only a single object will represent a value in your map.
Instead, you have to group the objects mapped to the same key (i.e. belonging to the same class) into a collection.
With stream API, you can do it by using the built-in collector Collectors.groupingBy(), which expects a classifier function that determines how to extract the key from the element of the stream. By default, values mapped to the same key will be grouped into a list.
Example (substitute the type Object with your super type):
List<Object> pets2 = List.of("foo", 9, "bar", 27, new HashSet<>());
Map<String, List<Object>> classNameToPet =
pets2.stream()
.collect(Collectors.groupingBy(obj -> obj.getClass().getSimpleName()));
classNameToPet.forEach((k, v) -> System.out.println(k + " : " + v));
Output
Integer : [9, 27]
String : [foo, bar]
HashSet : [[]]
Note :
The way you are using forEach() is not encouraged by the documentation, take a look at the code example provided in the paragraph "side-effects".
If you need to determine the actual class of your objects in order to interact with them, that means your usage of inheritance is wrong. If list pets2 has a generic parameter let's say Anymal, then you don't have to discriminate between the subtypes of Anymal, but take advantage from the polymorphism.
This would happen, if there are only two different classes in the list. Later objects with the same class as a previous on overwrite the previous one because the key is the same.
When you are iterating over the pets2 list and adding them to the animals map (Map<String, Object>), the put operation just overwrites the already inserted value for that specific class.
Eg:
pets2 = [dog1, dog2, cat1, cat2]
// The final animals map as per your method will be
animals = [{Dog, dog2}, {Cat, cat2}]
So, to handle the case of multiple repetitions of the same class objects in the pets list, we can change the code as follows
Map<String, List<Object>> animals = new HashMap<>();
pets2.forEach(pet -> animals.computeIfAbsent(pet.getClass().getSimpleName(), p -> new ArrayList<>()).add(pet));

stream of list groupinngBy to map

the example code I've been given is
public Map<String, List<Bier>> opzettenOverzichtBierenPerSoort() {
//TODO
return bieren.stream().collect(Collectors.groupingBy(Bier::getSoort, TreeMap::new, Collectors.toList()));
}
input is a list of beer objects and it return a map of the kind of beer with all the beers in it.
now my question. wat are the second and third arguments in the groupingBy? I get the first one which states what it's grouped by...but the second and third seem a bit random.
The second argument is a Supplier<M>, which is used to produce a Map instance.
The third argument is a downstream Collector, which specifies what to do with the Bier elements which belong to a single group.
If you run the single argument variant:
return bieren.stream().collect(Collectors.groupingBy(Bier::getSoort));
It will still collect the elements of each group into a List (that's the default behavior), but you don't have control over the type of Map that will map the String keys into the corresponding Lists.
In your 3 argument example, you request that the Map will be a TreeMap, which means the keys will be sorted.
The current implementation of the single argument variant:
return bieren.stream().collect(Collectors.groupingBy(Bier::getSoort));
is equivalent to:
return bieren.stream().collect(Collectors.groupingBy(Bier::getSoort, HashMap::new, Collectors.toList()));
which means the keys of the Map will not be sorted.

How to get an object instead of a list after applying groupingby on a list of Objects

I am doing a group by on a list of Objects as shown in the below code
Map<String, List<InventoryAdjustmentsModel>> buildDrawNumEquipmentMap = equipmentsAndCargoDetails.stream().
collect(Collectors.groupingBy(InventoryAdjustmentsModel :: getBuildDrawNum));
Now I know the values for all the keys would have only one element, so how can I reduce it to just
Map<String, InventoryAdjustmentsModel>
instead of having to iterate through or get the 0th element for all the keys.
You may use the toMap collector with a merge function like this.
Map<String, InventoryAdjustmentsModel> resultMap = equipmentsAndCargoDetails.stream().
collect(Collectors.toMap(InventoryAdjustmentsModel::getBuildDrawNum,
e -> e, (a, b) -> a));
Try it like this. By using toMap you can specify the key and the value. Since you said there were no duplicate keys this does not include the merge method. This means you will get an error if duplicate keys are discovered. Something I presumed you would want to know about.
Map<String, InventoryAdjustmentsModel> buildDrawNumEquipmentMap =
equipmentsAndCargoDetails.stream().
collect(Collectors.toMap(InventoryAdjustmentsModel::getBuildDrawNum,
model->model));

GroupBy on ArrayList of HashMap in java

I want to do a "group-by" on arrayList of HashMap Data structure. As my data is not fixed, so I don't have any fixed classes.
Data is shown as below.
[{"name":"laxman","state":"Karnataka","Mobile":9034782882},
{"name":"rahul","state":"Kerala","Mobile":9034782882},
{"name":"laxman","state":"karnataka","Mobile":9034782882},
{"name":"ram","state":"delhi","Mobile":9034782882}]
The above keys are not fixed, So, I can't have classes for it.
Data and formulas will be dynamical. But for now, I am taking this example to understand Stream.Collector on this data.
Now, I want to get the count on basis of name and state,
So basically I want to group-by on name and state and want to get count.
I tried to use Stream.Collector but am not able to achieve what I want.
You can accomplish this with Collectors.groupingBy, using a List as the key of the returned Map:
Map<List<String>, Long> result = yourListOfMaps.stream()
.collect(Collectors.groupingBy(
m -> Arrays.asList(String.valueOf(m.get("name")), String.valueOf(m.get("state"))),
Collectors.counting()));
This works well because all implementations of List in Java implement hashCode and equals consistently, which is a must for every class that is to be used as the key of any Map implementation.
You have to do groupingBy twice once on the key and once again on the value.
Map<String, Map<Object, Long>> map = listOfMap.stream().flatMap(a -> a.entrySet().stream())
.collect(Collectors.groupingBy(Map.Entry<String, String>::getKey,
Collectors.groupingBy(Map.Entry::getValue, Collectors.counting())));
Output
{mobile={9034782882=4}, name={rahul=1, laxman=2, ram=1}, state={Karnataka=2, delhi=1, Kerala=1}}

Filtering a map

I have a list of entries ,where entry has studentId and subjectId attributes.
List<Candidate> candidates
class Candidate {
...
String studentId;
String subjectId;
}
The objective is to derive a map of subjectId to list of studentIds,for those subjects which have been subscribed to by MORE than one student.
I can obviously create a temporary map by iterating over the candidates(a big count),remove single entries later - which seems a costly route.
Any other suggestions ?
We are using Java 1.7
Following this answer (corrected and completed) :
Map<String, Integer> map = candidates.stream()
.collect(Collectors.groupingBy(Candidate::getSubjectId))
.entrySet().stream().filter(x -> x.getValue().size()>1)
.collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue().size()));
You could also use candidates.parallelStream().
When you say 'costly', it's not exactly clear what you mean. - but I'll make an attempt.
With Java8, you can use the groupingBy construct to create a
Map<String, List<Candidate> groupedResults = candidates.stream().collect(Collectors.groupingBy(Candidate::getSubjectId));
and then simply filter out the entries where size <= 1. Rather simple and since it uses streams, memory efficiency should not be an issue.

Categories

Resources