How to get or create new map entry - java

I'm trying to write a simple utility function to get a value from a Map and, if it's not found to create a new value class and put that in the map.
It seems though very difficult to get the classes of the map's key and value at runtime and the best I can come up with is something horrible along the following lines.
Is there a better way?
private Object getOrCreate( Map<Object, Object> map, Object key, Class<?> mapValueClass ) {
Object value = map.get( key );
if (value == null) {
Constructor<?> con = mapValueClass.getConstructor( key.getClass() );
value = con.newInstance( key );
map.put( key, value );
}
return value;
}

You should ckeck out Map::getOrDefault and Map::computeIfAbsent (added in Java 8); those do pretty much exactly what your function is supposed to do. The difference between the two is that getOrDefault will accept an existing instance (created before the method is invoked) and return it if needed, but will not add it to the map, while computeIfAbsend accepts a function for lazily creating a new value, and will also add that value to the map.
Map<String, List<Integer>> map = new HashMap<>();
List<Integer> list1 = map.getOrDefault("foo", Collections.emptyList());
System.out.println(list1); // empty list
System.out.println(map); // map is still empty
List<Integer> list2 = map.computeIfAbsent("bar", s -> new ArrayList<Integer>());
System.out.println(list2); // empty list
System.out.println(map); // entry added to map
Assuming that you always want to create a new instance of the Value class with the key as parameter, and assuming that that class actually has such a constructor) you could e.g. use this:
YourClass obj = map.computeIfAbsent(key, YourClass::new);

Related

Least verbose way of calling Method on each object in two lists with a matching element

I have two lists that are populated from two different sources
List<MyObject> updatedObjs;
List<MyObject> currentObjs;
All of the elements in updatedObjs have the same string identifier as an element within the currentObjs list. This string Identifier can be simply accessed by getID() which returns a String.
What I would like to do is to find each match in the currentObjs list for each element in updatedObjs and then call a method passing the matching objects from both lists in.
I have figured out code for finding the match in both lists:
updatedObjs.stream().filter(updatedObj -> currentObjs.stream().anyMatch(currentObj -> updatedObj.getID().equals(currentObj.getID()))).collect(Collectors.toList());
But this will just find the matches...is there any way of calling anyMatch and then calling a method on both matching objects?
So I want to called a method like myMethod(currentObj, updatedObj);
I know I can do this with nested for loops but I am looking for a more elegant solution if one exists. Thanks
I think you need it:
updatedObjs.forEach(s -> currentObjs.stream().filter(s1 -> s.getId().equals(s2.getId)).forEach(s1 -> yourMethod(s, s1)));
My approach would be to create a Map whose key is the id and whose values are all the objects from each list that have that key. Then iterate over the entries in the map and call your myMethod() on each list that has more than 1 entry.
List<MyObj> updatedObjs = new ArrayList<>(); // assuming this has values
List<MyObj> currentObjs = new ArrayList<>(); // assuming this has values
List<MyObj> allObjs = new ArrayList( updatedObjs ); // create new list with references to the objects in updatedObjs
allObjs.addAll( currentObjs ); // add everything from currentObjs to our new list
// map of id -> list of all elements with that id
Map<String,List<MyObj>> idMap = allObjs
.stream()
.collect( Collectors.groupingBy( obj -> obj.getID() ) );
Now there's two different approaches below to calling the method
// you could stream the entry set, filter down to only entries with more than one value in the list and run the method on each
idMap.entrySet()
.stream()
.filter( ( mapEntry ) -> mapEntry.getValue().size() > 1 )
.forEach( mapEntry -> mapEntry.getValue().forEach( obj -> obj.getID() ) );
// I prefer a for-loop for this instead of streaming the entry set because it is a lot easier to read
for ( Map.Entry<String,List<MyObj>> entry : idMap.entrySet() ) { // iterate each entry
String key = entry.getKey();
List<MyObj> value = entry.getValue();
if ( value.size() > 1 ) {
// has more than one entry with the given key, so run myMethod() on each
value.forEach( obj -> obj.getID() );
}
}
By iterating over the first collection then the second, you're basically having an expensive algorithm O(n²)
You might want to consider something more straight forward like having a Map<String, List<MyObj>>
This hereunder is O(n)
Map<String, List<MyObj>> map = updatedObjs.stream()
.collect(Collectors.groupingBy(MyObj::getId));
currentObjs.forEach(obj -> map.get(obj.getId).add(obj)); // No Stream
map.forEach((k, v) -> myMethod(v.get(1), v.get(0));
That out of the way, the fact that you have two lists maintaining the state of the same entity is probably a bad idea. You should probably rethink your design from scratch.
Try the following approach:
void perform(List<MyObject> currentObjects, List<MyObject> updatedObjects) {
Map<String, MyObject> currentObjById = currentObjects.stream()
.collect(Collectors.toMap(MyObject::getID, Function.identity()));
Map<String, MyObject> updatedObjById = updatedObjects.stream()
.collect(Collectors.toMap(MyObject::getID, Function.identity()));
for (Map.Entry<String, MyObject> entry : currentObjById.entrySet()) {
MyObject updatedObj = updatedObjById.get(entry.getKey());
if (updatedObj != null) {
myMethod(entry.getValue(), updatedObj);
}
}
}
It is more efficient than the solutions which use anyStream. This will work provided that you have no two elements with the same ID in each of the lists.

map.getOrDefault().add() in Java not works

The traditional code works well like below:
Map<Integer, List<Integer>> map = new HashMap<>();
if (!map.containsKey(1)) {
map.put(1, new ArrayList<>());
}
map.get(1).add(2);
Now I'd like to try the magic of getOrDefault:
map.getOrDefault(1, new ArrayList<>()).add(2);
But if I use the above line, then map.get(1) is null.
Why?
Because getOrDefault, as its name suggests, only gets stuff from the map. It doesn't adds a new KVP to the map. When the key is not present, the default value you pass to getOrDefault is returned, but not added to the map, so you'd be adding 2 to an array list that is thrown away immediately.
In other words, this is what your getOrDefault code is doing:
ArrayList<Integer> value;
if (!map.containsKey(1)) {
value = new ArrayList<>();
} else {
value = map.get(1);
}
value.add(2);
You should use computeIfAbsent instead. This method actually adds the return value from the function to the map if the key is not present:
map.computeIfAbsent(1, x -> new ArrayList<>()).add(2);
or you could do:
if(!map.contansKey(1)) map.put(1, new ArrayList<>());
map.get(1).add(2);
so you can save those lines ;)

How to put together multiple Maps <Character, Set<String>> without overriding Sets

In my project I am using two maps Map<Character, Set<String>>.
map1 - is temporally holding needed values
map2 - is summing all data from map1 after each loop
for example i got:
map2 = (B; Beryllium, Boron, Bromine)
map2 = (H; Hellum, Hydrogen, Hafnium)
now new map1 is:
map1 = (B; Bismuth)
map1 = (O; Oxygen)
In my code adding Oxygen as new entry is ok, but adding new entry for B ends by overraidding existing data in values and leave me only Bismuth.
My code:
while (iterator.hasNext()) {
Set<String> words = new TreeSet<>();
String word = iterator.next();
char[] wordChars = word.toCharArray();
//some code
words.add(word);
map1.put(wordChars[i], words);
}
map2.putAll(map1);
I tought about using .merge but I have no idea how to use it with Sets as values, and I cannot use simple Strings with concat.
You can use Map#merge like this:
Map<String, Set<String>> map1; // [key="B";values=["Beryllium", "Boron", "Bromine"]]
Map<String, Set<String>> map2; // [key="B";values=["Bismuth"] key="I";values=["Iron"]]
for (Entry<String, Set<String>> entry : map2.entrySet()) {
map1.merge(entry.getKey(), entry.getValue(), (s1, s2) -> {s1.addAll(s2); return s1;});
}
//map1 = [key="B";values=["Beryllium", "Boron", "Bromine", "Bismuth"] key="I";values=["Iron"]]
Map::compute is probably what you're looking for. This gives you a way to map any existing value (if there is one), or provide one if not.
For example, in your case something like the following would probably suffice:
oldMap.compute("B", current -> {
if (current == null) {
// No existing entry, so use newMap's one
return newMap.get("B");
} else {
// There was an existing value, so combine the Sets
final Set<String> newValue = new HashSet<>(current);
newValue.addAll(newMap.get("B"));
return newValue;
}
});
There's also MultiValueMap and Multimap from spring and guava respectively (if you're ok bringing in dependencies) which cover this case with less work already.
Temporary map1 will not be needed in this case. Get the set for that character, if null create a new set. Add the word to that set and put in the map:
while (iterator.hasNext()) {
String word = iterator.next();
//some code
Set<String> words = map2.get(word.charAt(0));
if(words == null) {
words = new TreeSet<>();
}
words.add(word);
map2.put(word.charAt(0), words);
}
When using the merge() function, if the specified key is not already associated with a value or the value is null, it associates the key with the given value.
Otherwise, i.e if the key is associated with a value, it replaces the value with the results of the given remapping function. So in order to do not overwrite the old value you must write your remapping function so that it combines the old and new values.
To do so replace this line :
map2.putAll(map1);
with
map1.forEach( (key, value)->{
map2.merge(key, value, (value1,value2) -> Stream.of(value1,value2)
.flatMap(Set::stream)
.collect(Collectors.toSet()));
});
This will iterate over map1 and add echh key which is not present into map2 and associate it with the given value and for each key which is already present it combines the old values and new values.
Alternative you can also work with Map.computeIfPresent and Map.putIfAbsent
map1.forEach( (key, value)->{
map2.computeIfPresent(key, (k,v) -> Stream.of(v,value).flatMap(Set::stream).collect(Collectors.toSet()));
map2.putIfAbsent(key, value);
});

Java - Adding another String value to existing HashMap Key without overwriting?

I was wondering if someone would be able to help with regards to adding another String value to an existing key within a HashMap in Java?
I understand that you can add a Key-Value pair using the this.put("String", "String") method. However, it overwrites the existing value, whereas I would like multiple values stored and paired, with the same key?
Thanks for your help.
What are you hoping to achieve here?
A Map (the HashMap) in your case is a direct "mapping" from one "key" to another value.
E.g.
"foo" -> 123
"bar" -> 321
"far" -> 12345
"boo" -> 54321
This means that if you were to try:
myHashMap.get("foo");
It would return the value 123 (of course, the type of the value you return can be anything you want).
Of course, this also means that any changes you make to the value of the key, it overrides the original value you assigned it, just like changing the value of a variable will override the original one assigned.
Say:
myHashMap.put("foo", 42);
The old value of "foo" in the map would be replaced with 42. So it would become:
"foo" -> 42
"bar" -> 321
"far" -> 12345
"boo" -> 54321
However, if you need multiple String objects that are mapped from a single key, you could use a different object which can store multiple objects, such as an Array or a List (or even another HashMap if you wanted.
For example, if you were to be using ArrayLists, when you are assigning a value to the HashMap, (say it is called myHashMap), you would first check if the key has been used before, if it hasn't, then you create a new ArrayList with the value you want to add, if it has, then you just add the value to the list.
(Assume key and value have the values you want)
ArrayList<String> list;
if(myHashMap.containsKey(key)){
// if the key has already been used,
// we'll just grab the array list and add the value to it
list = myHashMap.get(key);
list.add(value);
} else {
// if the key hasn't been used yet,
// we'll create a new ArrayList<String> object, add the value
// and put it in the array list with the new key
list = new ArrayList<String>();
list.add(value);
myHashMap.put(key, list);
}
You can do like this!
Map<String,List<String>> map = new HashMap<>();
.
.
if(map.containsKey(key)){
map.get(key).add(value);
} else {
List<String> list = new ArrayList<>();
list.add(value);
map.put(key, list);
}
Or you can do the same thing by one line code in Java 8 style .
map.computeIfAbsent(key, k ->new ArrayList<>()).add(value);
Would you like a concatenation of the two strings?
map.put(key, val);
if (map.containsKey(key)) {
map.put(key, map.get(key) + newVal);
}
Or would you like a list of all the values for that key?
HashMap<String,List<String>> map = new HashMap<String,List<String>>();
String key = "key";
String val = "val";
String newVal = "newVal";
List<String> list = new ArrayList<String>();
list.add(val);
map.put(key, list);
if (map.containsKey(key)) {
map.get(key).add(newVal);
}
As others pointed, Map by specification can have only one value for a given key. You have 2 solutions:
Use HashMap<String, List<String>> to store the data
Use Multimap which is provided by 3rd party Google Collections lib
As described in Map interface documentation Map contains a set of keys, so it is not capable of containing multiple non-unique keys.
I suggest you to use lists as values for this map.
Store value as list under map So if key is test and there are two values say val1 and val2 then key will be test and value will be list containing val1 and val2
But if your intention is to have two separate entries for same key, then this is not Map is designed for. Think if you do map.get("key"), which value you expects
You could use Map<String, Collection<String>> but adding and removing values would be cumbersome . Better way is using guava Multimap - a container that allows storing multiple values for each key.
You can't directly store multiple values under a single key, but the value associated with a key can be any type of object, such as an ArrayList, which will hold multiple values. For example:
import java.util.ArrayList;
import java.util.HashMap;
public class HashMapList {
HashMap<String, ArrayList<String>> strings = new HashMap<String, ArrayList<String>>();
public void add(String key, String value) {
ArrayList<String> values = strings.get(key);
if (values == null) {
values = new ArrayList<String>();
strings.put(key, values);
}
values.add(value);
}
public ArrayList<String> get(String key) {
return strings.get(key);
}
public static void main(String[] argv) {
HashMapList mymap = new HashMapList();
mymap.add("key", "value1");
mymap.add("key", "value2");
ArrayList<String> values = mymap.get("key");
for (String value : values) {
System.out.println(value);
}
}
}
it's impossible,because String is immutable if you use the String as the key of map the same key's value has the same hashcode value.

Adding an Object to a collection

I am learning Hashmaps in Java, so I have a simple java program that creates an account. My problem is when it comes to storing the new accounts in a collection, I am trying to do it using a hashmap but just can't figure out where to go.
HashMap<String,CurrentAccount> m = new HashMap<String,String>();
if (Account.validateID(accountID)) {
CurrentAccount ca = new CurrentAccount(cl,accountID, sortCode, 0);
I am unsure of the next stage to add this account to the hashmap I have tried a couple of different ways but always end up with an error.
You have an error with your instantiation statement. The map's type is HashMap<String, CurrentAccount>, but you are instantiating HashMap<String,String>.
To fix this, change your instantiation statement to correspond to the map's type, like the following:
HashMap<String, CurrentAccount> m = new HashMap<String, CurrentAccount>();
Or if you are using JDK 1.7+, you could use diamond notation instead (see Generic Types for more information):
HashMap<String, CurrentAccount> m = new HashMap<>();
In order to add items to the map, you can use Map#put(K, V):
m.put(accountID, ca);
In order to get a value, you can use Map#get(Object):
CurrentAccount ca = m.get(accountID);
See JDK 1.7 Map documentation for more information about maps.
As for the question made by the OP in the comments of this answer, in order to access the map (or any other type) in multiple methods, it has to be declared as a class field:
public class TestClass {
Map<String, CurrentAccount> accountMap;
public TestClass() {
accountMap = new HashMap<String, CurrentAccount>();
}
public void method1() {
// You can access the map as accountMap
}
public void method2() {
// You can also acces it here
}
}
The map declaration is incorrect, as you're typing the value to two different objects. Change the declaration to:
Map<String,CurrentAccount> m = new HashMap<String,CurrentAccount>();
Then, presuming the accountID value is a string, it should be as simple as...
m.put( accountID, ca );
Altogether you'll have:
Map<String,CurrentAccount> m = new HashMap<String,CurrentAccount>();
if (Account.validateID(accountID)) {
CurrentAccount ca = new CurrentAccount(cl,accountID, sortCode, 0);
m.put( accountID, ca );
}
Use put(key, value); See HashMap javadoc
m.put("SomeIdentifierString", ca);
Then whenever you want to access that particular object. Use the key to obtain it
CurrentAccount account = m.get("SomeIdentifierString");
If you want to iterate through the entire map to get key and values you can do this
for (Map.Entry<String, CurrentAccount> entry : m.entrySet()){
String s = entry.getKey();
CurrentAccount accuont = entry.getValue();
// do something with them
}
Your code does not compile because you try to initialize the hasmap bound to the type String and Account to an hashmap of type String String for (key and value type)
HashMap<String, CurrentAccount> accountsMap = new Hashmap<String, String>()
should be
HashMap<String, CurrentAccount> accountsMap = new Hashmap<String, CurrentAccount>()
The first argument is the type for the key value the second is the type of the associated value for the key
To find a value within your hashmap you can use the following code snipped
for (String key : accountsMap.keySet().iterator()) {
CurrentAccount current = accounts.get(key);
}
where accountsMap is your HashMap.

Categories

Resources