Copying keys from one Map to another - java

I have two maps. One with default values of 0 and one with set values.
For example:
Map<String,Integer> defaultTaskScores = new HashMap<String, Integer>(){{
put("Phalanxing", 0);
put("Shieldwalling",0);
put("Tercioing",0);
put("Wedging",0);
}};
Map<String,Integer> taskScores = new HashMap<String, Integer>(){{
put("Phalanxing", 90);
put("Shieldwalling",56);
put("Wedging",24);
}};
I want to put into taskScores a pair of key-value from defaultTaskScores which key's isn't in taskScores. For this example it's putting Tercioing with value of 0.
taskScores maps are in the list
List<CourseResult> courseResultList;
public class CourseResult {
private final Person person;
private final Map<String, Integer> taskResults;
public CourseResult(final Person person, final Map<String, Integer> taskResults) {
this.person = person;
this.taskResults = taskResults;
}
}

You could iterate over defaultTaskScores and use putIfAbsent to add the missing keys to taskScores:
defaultTaskScores.keySet().forEach(k -> taskScores.putIfAbsent(k, 0));
EDIT:
An alternate approach could be to apply the default value when retrieving a score from the map. Instead of calling taskScaores.get(someKey), you could use taskScores.getOrDefault(someKey, 0).

Only put in the Map those values if the key is not already there:
for(Map.Entry<String, Integer> entry : defaultTaskScores.entrySet()) {
taskScores.putIfAbsent( entry.getKey(), entry.getValue() );
}

You can create iterate over the entries of defaultTaskScores with enhanced for-loop using entry set as a source, or by invoking forEach() method on thedefaultTaskScores map directly.
To update taskScores you can use Java 8 methods:
putIfAbsent():
defaultTaskScores.forEach(taskScores::putIfAbsent);
or computeIfAbsent():
defaultTaskScores.forEach((k, v) -> taskScores.computeIfAbsent(k, (key) -> v));
In case if you're not comfortable with lambda expressions and method references, have a look at this tutorial created by Oracle.
Sidenote: avoid using obsolete double brace initialization, it creates an anonymous class under the hood and might cause memory leaks. Since Java 9 we have a family of overloaded factory methods Map.of() and method Map.ofEntries() which produce an immutable map. When you need a map which is mutable like in this case, you can wrap it with a HashMap, like that:
Map<String, Integer> taskScores = new HashMap<>(Map.of(
"Phalanxing", 90, "Shieldwalling",56,"Wedging",24
));

Related

How to construct a Map using with object properties as key using java stream and toMap or flatMap

I have list of objects and need to create a Map have key is combination of two of the properties in that object. How to achieve it in Java 8.
public class PersonDTOFun {
/**
* #param args
*/
public static void main(String[] args) {
List<PersonDTO> personDtoList = new ArrayList<>();
PersonDTO ob1 = new PersonDTO();
ob1.setStateCd("CT");
ob1.setStateNbr("8000");
personDtoList.add(ob1);
PersonDTO ob2 = new PersonDTO();
ob2.setStateCd("CT");
ob2.setStateNbr("8001");
personDtoList.add(ob2);
PersonDTO ob3 = new PersonDTO();
ob3.setStateCd("CT");
ob3.setStateNbr("8002");
personDtoList.add(ob3);
Map<String,PersonDTO> personMap = new HashMap<>();
//personMap should contain
Map<String, PersonDTO> personMap = personDtoList.stream().collect(Collectors.toMap(PersonDTO::getStateCd,
Function.identity(),(v1,v2)-> v2));
}
}
In the above code want to construct personMap with key as StateCd+StateNbr and value as PersonDTO. As existing stream and toMap function only support single argument function as key can't able to create a key as StateCd+StateNbr.
Try it like this.
The key argument to map is the key and a concatenation of the values you described.
The value is simply the object
Map<String, PersonDTO> personMap =
personDtoList
.stream()
.collect(Collectors.toMap(p->p.getStateCd() + p.getStateNbr(), p->p));
If you believe you will have duplicate keys, then you have several choices include a merge function.
the one shown below preserves the value for the first key (existing) encountered.
Map<String, PersonDTO> personMap =
personDtoList
.stream()
.collect(Collectors.toMap(p->p.getStateCd() + p.getStateNbr(), p->p,
(existingValue, lastestValue)-> existingValue));
the next one saves all instances of PersonDTO and puts same key values in a list.
Map<String, List<PersonDTO>> personMap =
personDtoList
.stream()
.collect(Collectors.groupingBy(p->p.getStateCd() +
p.getStateNbr()));
as the two answers above, you can use the toMap function that gets 2 functions as input, the first one is the way to get a unique key, the second is how to get the data.
We prefer to hold the unique key in the class its self and to call the function for the key directly from that class using the Class::Method as the function for the keys, it makes the code much more readable instead of using nested lambda functions
so using WJS example:
Map<String, PersonDTO> personMap =
personDtoList
.stream()
.collect(Collectors.toMap(p->p.getStateCd() + p.getStateNbr(), p->p));
Map<String, PersonDTO> personMap =
personDtoList
.stream()
.collect(Collectors.toMap(PersonDTO::getUniqueKey, p->p));
Note that this is exactly the same as the above logic wise
I have fixed with following implementation.
Function<PersonDTO, String> keyFun = person -> person.getStateCd()+person.getStateNbr();
Map<String, PersonDTO> personMap = personDtoList.stream().collect(Collectors.toMap(keyFun,
Function.identity(),(v1,v2)-> v2));
This will work in case while creating dynamic key if we encounter duplicate key.

Create Map with provided set of keys and default values for them

I have a method that receives List<String> keys and does some computations on these and at the end it returns Map<String, String> that has keys from that List and values computed or not. If value cannot be computed I want to have empty String for that key.
The clearest way would be to create Map containing all the keys with default values (empty String in that case) at the start of computations and then replace computed values.
What would be the best way to do so? There is no proper initializer in Collections API I think.
The easiest answer came to me seconds ago:
final Map<String, String> labelsToReturn = keys.stream().collect(toMap(x -> x, x -> ""));
solves the problem perfectly.
Just use stream and toMap with key / value mapping to initialize the map.
I advise you to use Stream API Collectors.toMap() method. There is a way:
private static void build(List<String> keys) {
Map<String, String> defaultValues = keys.stream().collect(
Collectors.toMap(key -> key, key -> "default value")
);
// further computations
}
Map<String,String> map = new HashMap<>();
String emptyString = "";
for(String key:keys){
Object yourcomputation = emptyString;
//asign your computation to new value base on key
map.put(key,yourcomputation);
}

Best approach to delete a set of entries in a Map

Is there any fast and reliable way/approach to remove a set of entries based on an attribute on the entry's value. Below approach loops every entry which in undesirable.
e.g: ConcurrentHashMap - Entries will be in millions
Iterator<Map.Entry<String, POJO>> iterator = map.entrySet().iterator();
for (Iterator<Map.Entry<String, POJO>> it = iterator; it.hasNext(); ) {
Map.Entry<String, POJO> entry = it.next();
POJO value = entry.getValue();
if (value != null && *attribute*.equalsIgnoreCase(value.getAttribute())) {
it.remove();
}
}
There is no better way of mutating map in place without utilizing additional data structures. What you are basically asking for is secondary index like employed in databases, where you have pointers to entries based on some non-primary key properties. If you don't want to store extra index, there is no easier way then to iterate through all entries.
What I would suggest you to look into is composing map view over your original map. For example something like (using guava)
Map<String,POJO> smallerMap = Maps.filterValues(map,
v -> !attribute.equalsIgnoreCase(v.getAttribute())
);
You will need to be careful with such view (don't call size() on it for example), but for access etc it should be fine (depending on your exact needs, memory constraints etc).
And side note - please note I have removed null check for value. You cannot store null values in ConcurrentHashMap - and it is also not that great idea in normal map as well, better to remove entire key.
There are two solutions that I can think of
First solution:
Create another map for storing the object hashcode as key and corresponding key as value. The structure could be like as below
Map<Integer, String> map2 = new HashMap<>();
Here is working solution. Only drawback of this is getting a unique hashcode might be tough if there are large number of objects.
import java.util.HashMap;
import java.util.Map;
public class DualHashMap {
public static void main(String a[]){
Map<String, Pojo> map = new HashMap<>();
Map<Integer, String> map2 = new HashMap<>();
Pojo object1 = new Pojo();
Pojo object2 = new Pojo();
map.put( "key1", object1);
map.put( "key2", object2);
map2.put(object1.hashCode(), "key1");
map2.put(object2.hashCode(), "key2");
// Now let say you have to delete object1 you can do as follow
map.remove(map2.get(object1.hashCode()));
}
}
class Pojo{
#Override
public int hashCode(){
return super.hashCode(); //You must work on this yourself, and make sure hashcode is unique for each object
}
}
2nd solution:-
Use the solution provided for dual hashmap by Guava or Apache
The Apache Commons class you need is BidiMap.
Here is another for you by Guava

Java 8 re-map with modified value

I'm about to get a grasp on the new Java 8 stream and lambda options, but there are still a few subtleties that I haven't yet wrapped my mind around.
Let's say that I have a map where the keys are the names of people. The value for each name is a map of ages and Person instances. Further assume that there does not exist more than one person with the same name and age.
Map<String, NavigableMap<Long, Person>> names2PeopleAges = new HashMap<String, NavigableMap<Long, Person>>();
After populating that map (elsewhere), I want to produce another map of the oldest person for each name. I want to wind up with a Map<String, Person> in which the keys are identical to those in the first map, but the value for each entry is the value of the value map for which the key of the value map has the highest number.
Taking advantage of the fact that a NavigableMap sorts its keys, I can do this:
Map<String, Person> oldestPeopleByName = new HashMap<String, Person>();
names2PeopleAges.forEach((name, peopleAges) -> {
oldestPeopleByName.put(name, peopleAges.lastEntry().getValue());
});
Question: Can I replace the last bit of code above with a single Java 8 stream/collect/map/flatten/etc. operation to produce the same result? In pseudo-code, my first inclination would be:
Map<String, Person> oldestPeopleByName = names2PeopleAges.forEachEntry().mapValue(value->value.lastEntry().getValue());
This question is meant to be straightforward without any tricks or oddities---just a simple question of how I can fully leverage Java 8!
Bonus: Let's say that the NavigableMap<Long, Person> above is instead merely a Map<Long, Person>. Could you extend the first answer so that it collects the person with the highest age value, now that NavigableMap.lastEntry() is not available?
You can create a Stream of the entries and collect it to a Map :
Map<String, Person> oldestPeopleByName =
names2PeopleAges.entrySet()
.stream()
.collect (Collectors.toMap(e->e.getKey(),
e->e.getValue().lastEntry().getValue())
);
Now, without lastEntry :
Map<String, Person> oldestPeopleByName =
names2PeopleAges.entrySet()
.stream()
.collect (Collectors.toMap(e->e.getKey(),
e->e.getValue().get(e.getValue().keySet().stream().max(Long::compareTo)))
);
Here, instead of relying on lastEntry, we search for the max key in each of the internal Maps, and get the corresponding Person of the max key.
I might have some silly typos, since I haven't actually tested it, by in principle it should work.
A similar question is asked before, and I provided there my answers https://stackoverflow.com/a/75004577/6777695 The answer there is slighty different than the one accepted here. I think the remapping of a key or/and value should belong in the map part of a stream instead of the collect as the map function for a stream is designed to do the actual transforming of data. In your use case I would suggest the following code snippet:
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
public class App {
public static void main(String[] args) {
Map<String, Person> oldestPeopleByName = namesToPeopleAges.entrySet().stream()
.map(entry -> Map.entry((entry.getKey(), entry.getValue().lastEntry().getValue()))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
}
}

How to swap keys and values in a Map elegantly

I already know how to do it the hard way and got it working - iterating over entries and swapping "manually". But i wonder if, like so many tasks, this one can be solved in a more elegant way.
I have read this post, unfortunately it does not feature elegant solutions. I also have no possibility to use any fancy Guava BiMaps or anything outside the jdk (project stack is already defined).
I can assume that my map is bijective, btw :)
Map<String, Integer> map = new HashMap<>();
Map<Integer, String> swapped = map.entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
If you don't have a choice to use a third party library, I don't consider the following code so ugly (though some scripting languages do have elegant ways of doing it):
//map must be a bijection in order for this to work properly
public static <K,V> HashMap<V,K> reverse(Map<K,V> map) {
HashMap<V,K> rev = new HashMap<V, K>();
for(Map.Entry<K,V> entry : map.entrySet())
rev.put(entry.getValue(), entry.getKey());
return rev;
}
The standard API / Java runtime doesn't offer a bi-directional map, so the only solution is to iterate over all entries and swap them manually.
What you can do is create a wrapper class which contains two maps and which does a dual put() internally so you have fast two views on the data.
[EDIT] Also, thanks to open source, you don't have to include a third party library, you can simply copy the classes you need into your own project.
Maps are not like lists, which can be reversed by swapping head with tail.
Objects in maps have a computed position, and using the value as key and the key as value would requiere to re-compute the storage place, essentialy building another map. There is no elegant way.
There are, however, bidirectional maps. Those may suit your needs. I'd reconsider using third-party libraries.
There are some jobs that can be simplified to a certain point and no more. This may be one of them!
If you want to do the job using Java collections apis only then brute force is the way to go - it will be quick (unless the collection is huge) and it will be an obvious piece of code.
As a hint to answer
https://stackoverflow.com/a/42091477/8594421
This only works, if the map is not a HashMap and does not contain duplicate values.
Map<String,String> newMap = oldMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
throws an exception
java.lang.IllegalStateException: Duplicate key
if there are values more than once.
The solution:
HashMap<String,String> newMap = new HashMap<>();
for(Map.Entry<String,String> entry : oldMap.entrySet())
newMap.put(entry.getValue(), entry.getKey());
// Add inverse to old one
oldMap.putAll(newMap);
If you had access to apache commons-collections, you could have used MapUtils.invertMap.
Note: The behaviour in case of duplicated values is undefined.
(Replying to this as this is the first google result for "java invert map").
Java stream API provides nice set of APIs that would help you with this.
If the values are unique then the below would work. When I mean values, I mean the V in the Map<K, V>.
Map<String, Integer> map = new HashMap<>();
Map<Integer, String> swapped = map.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
If the values are not unique, then use below:
Map<Integer, List<String>> swapped = map.entrySet()
.stream()
.collect(Collectors.groupingBy(Map.Entry::getValue, Collectors.mapping(Map.Entry::getKey, Collectors.toList())));
Thanks Nikita and FreyaZ. Posting as new answer as there were so many edit queues for Nikita's Answer
This will work for duplicate values in the map also, but not for HashMap as values.
package Sample;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class Sample {
public static void main(String[] args) {
Map<String,String> map = new HashMap<String,String>();
Map<String, Set<String> > newmap = new HashMap<String, Set<String> >();
map.put("1", "a");
map.put("2", "a");
map.put("3", "b");
map.put("4", "b");
System.out.println("before Reversing \n"+map.toString());
for (Map.Entry<String, String> entry : map.entrySet())
{
String oldVal = entry.getValue();
String oldKey = entry.getKey();
Set<String> newVal = null;
if (newmap.containsKey(oldVal))
{
newVal = newmap.get(oldVal);
newVal.add(oldKey);
}
else
{
newVal= new HashSet<>();
newVal.add(oldKey);
}
newmap.put(oldVal, newVal);
}
System.out.println("After Reversing \n "+newmap.toString());
}
}

Categories

Resources