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.
Related
I have a list of Profile objects List<Profile> list.
Which I need to convert into a LinkedHashMap<String, String>.
Where object Profile is consisted of:
public class Profile {
private String profileId;
private String firstName;
private String lastName;
}
I have tried the following:
Map<String, String> map = list.stream()
.collect(Collectors.toMap(Profile::getFirstName,
Profile::getLastName));
But it did not work, I'm getting a compilation error:
Incompatible parameter types in method reference expression
Incompatible parameter types in method reference expression
Make sure that you're not using a list of row type as a stream source. I.e. check if the generic type parameter is missing: List list (it has to be List<Profile> list), otherwise all elements of the list as being of type Object and methods from the Profile class would not be accessible.
Collecting into a LinkedHashMap
By default, toMap provides you with a general purpose implementation of the Map (for now it's HashMap but it might change in the future).
In order to collect stream elements into a particular implementation of the Map interface, you need to use a flavor of Collectors.toMap() that expects four arguments:
keyMapper - a mapping function to produce keys,
valueMapper - a mapping function to produce values,
mergeFunction - function that is meant to resolve collisions between value associated with the same key,
mapFactory - a supplier providing a new empty Map into which the results will be inserted.
In the code below, mergeFunction isn't doing anything useful, it just has to be present in order to utilize the version of toMap() that allows to specify the mapFactory.
Map<String, String> map = list.stream()
.collect(Collectors.toMap(
Profile::getFirstName,
Profile::getLastName,
(left, right) -> left,
LinkedHashMap::new));
Note if there could be cases when more than one value gets associated with the same key, you need either to provide a proper implementation of mergeFunction (to peek a particular value or aggregate values, etc.), or use groupingBy() as a collector, which will allow to preserve all values associated with a particular key.
I have a HashMap defined with something like this:
Map<Foo, List<Bar>> = new HashMap();
I am trying to do a reverse search of the Hashmap using the Bar to get the Foo.
I am wanting to do something like this:
if(ArrayListBar.contains(bar)) {
return Foo;
} else {
return null;
}
Is this achievable in HashMap or is there a better way to deal with this without using the HashMap?
You can do it with Map iteration.
private Foo getKeyByValue(Map<Foo, List<Bar>> map, Bar bar){
for (Map.Entry<Foo, List<Bar>> entry : map.entrySet()){
if (entry.getValue().contains(bar)){
return entry.getKey();
}
}
return null;
}
You iterate for each entry on the map and you return the Key when the array list contains the entered bar value.
Note that your Bar class should implement the equals method so the entry.getValue().contains(bar) can be evaluated if the bar in the List with the bar on the method input are different objects.
Update: Added missing return null statement when no map element is found.
The best approach can be different under different circumstances.
You can do it by iterating the map and checking whether the list of each entry contains the value or not. But that's Ok only if the map is not too big and the lists in it or either not too big or sorted.
But if the size of the map is big or the lists are big and not sorted and you need to do multiple lookups, it is better to create a reverse map Map.
In that case you have to make sure the hashkey and equals method of Bar are implemented correctly.
If the same value (Bar) can be in multiple lists for different keys (Foo), the reverse map might also require a list of values: Map>.
Depending on the situation, you can create the reverse map while building the original map or afterwards when doing the first lookup and cache it for reuse in later lookups.
When thread safety is involved, it is preferred to create it when creating the original map because in that case you don't need to worry about thread safety of the lookup method which is a problem when creating the reverse map during the first lookup.
I have a list of objects. I need to update a single object from the list that match my filter. I can do something like below:
List<MyObject> list = list.stream().map(d -> {
if (d.id == 1) {
d.name = "Yahoo";
return d;
}
return d;
});
But my worry is i am like iterating through the whole list which may be up to 20k records. I can do a for loop then break, but that one I think also will be slow.
Is there any efficient way to do this?
Use findFirst so after finding the first matching element in the list remaining elements will not be processed
Optional<MyObject> result = list.stream()
.filter(obj->obj.getId()==1)
.peek(o->o.setName("Yahoo"))
.findFirst();
Or
//will not return anything but will update the first matching object name
list.stream()
.filter(obj->obj.getId()==1)
.findFirst()
.ifPresent(o->o.setName("Yahoo"));
You can use a Map instead of a list and save the id as a key.
https://docs.oracle.com/javase/8/docs/api/java/util/Map.html
Then you can extract it with O(1).
It depends on how often you need to perform this logic on your input data.
If it happens to be several times, consider using a Map as suggested by Raz.
You can also transform your List into a Map using a Stream:
Map<Integer, MyObject> map = list.stream()
.collect(Collectors.toMap(
MyObject::getId
Function.identity()
));
The first argument of toMap maps a stream item to the corresponding key in the map (here the ID of MyObject), and the second argument maps an item to the map value (here the MyObject item itself).
Constructing the map will cost you some time and memory, but once you have it, searching an item by ID is extremely fast.
The more often you search an item, the more constructing the map first pays off.
However, if you only ever need to update a single item and then forget about the whole list, just search for the right element, update it and you're done.
If your data is already sorted by ID, you can use binary search to find your item faster.
Otherwise, you need to iterate through your list until you find your item. In terms of performance, nothing will beat a simple loop here. But using Stream and Optional as shown in Deadpool's answer is fine as well, and might result in clearer code, which is more important in most cases.
.stream().peek(t->t.setTag(t.getTag().replace("/","")));
Do anything you want with peek() meyhod
I had a HashMap called map which stored Characters as the key and Integers as the value which I then stored into an ArrayList called entries using the following code:
Set<Entry<Character, Integer>> s = map.entrySet();
ArrayList<Entry<Character, Integer>> entries = new ArrayList<>(s);
Now I am trying to sort these entries based on the Integer value, not the key. I tried to use a lambda expression to implement the Comparator interface, but it is not working. This is my code:
Collections.sort(sortedEntries, (sortedEntries.get(0), sortedEntries.get(1)) -> {
sortedEntries.get(0).getValue().compareTo(sortedEntries.get(1).getValue())
});
These are the errors I get:
Multiple markers at this line
Syntax error, insert ")" to complete Expression
The method sort(List, Comparator) in the type Collections is not applicable for the arguments (ArrayList>, Map.Entry, Map.Entry)
Syntax error on token "->", ; expected
You could sort the list by the values this way:
list.sort(Comparator.comparing(Entry::getValue));
This is strictly as per your code pls see
Collections.sort(sortedEntries, (Entry<Character,Integer> o1, Entry<Character,Integer> o2)-> {return o1.getValue().compareTo(o2.getValue());});
There is nothing special about the map, the list, or the type you're trying to sort. You sort such a list the same way you sort any List whose elements are of a type that doesn't implement Comparable - by using a Comparator, like you're attempting to do. So you're on the right track.
Perhaps if you aren't comfortable with lambda expressions, try implementing the Comparator interface instead. The type system will force you to "do it right", rather than relying on you to get it right so it can infer the types for you.
(I'm using the entry's key here, but you can just as well use the value)
public class EntryKeyComparator implements Comparator<Entry<Character,Integer>> {
#Override
public int compare(Entry<Character,Integer> a, Entry<Character,Integer> b) {
return a.getKey().compareTo(b.getKey());
}
}
Now, if you really want to use lambda expressions, you need only look at the compare method in this implementation. The lambda expression must take two parameters, a and b, and it must return an int. Thus, you'd get something like:
Collections.sort(list, (a, b) -> a.getKey().compareTo(b.getKey()));
But since you're already in the Java 8 world, you might as well use the static comparing() method on the Comparator class. It takes as argument a Function that extracts the key you want to use to compare the elements by. In my examples, I've compared the entries by their keys via the getKey(). We can reference this method with the :: operator, so you end up with an expression like this (using the new sort() method on the List interface):
list.sort(Comparator.comparing(Entry::getKey));
You can sort the entities using Java Stream API like this:
Set<Entry<Character, Integer>> s = map.entrySet();
List<Entry<Character, Integer>> sortedEntries = s.stream()
.sorted((a, b)-> Integer.compare(a.getValue(), b.getValue()))
.collect(Collectors.toList());
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>>.