HashMap<String,Integer> data structure, but values are added instead of replaced

Viewed 699

I am seaching for a data structure that is almost exactly a HashMap<String,Integer>, but the problem with HashMaps is that most of the data stored in key value pairs is lost by calling the putAll() method on two HashMaps, due to the replacement behavior of putVal() in line 655 of the java/util/HashMap.java.

This is basically the change that I want:

    if (e != null) { // existing mapping for key
         V oldValue = e.value;
         if (!onlyIfAbsent || oldValue == null)
--            e.value = value;
++            e.value = value + oldValue;
         afterNodeAccess(e);
         return oldValue;
    }

Is there an existing data structure, that I've overlooked that would do such a thing, or how do I create a class that is basically a HashMap with that one change?

I've already tried to code something, but doen't work how I want it to... In fact it doen't matter if I set the put method on @Override, do it like that, or delete it completely - the replacing behavior ofcourse stays the same, because putAll() uses putVal() that I can't reach / change from the outside - or I at least don't know how...

 /**
  * doesn't work, putAll() uses putVal() that I can't reach
  */
 public class SumHashMap<K> extends HashMap<K, Integer> {
    private static final long serialVersionUID = 1L;

    public Integer put(K key, Integer value) {
        Integer oldValue = get(key);
        if (oldValue == null)
            return super.put(key, value);
        return super.put(key, oldValue + value);
    }
}

Thanks in advance

Additional info:

  • I want to use the putAll() function in the reduction of a stream out of custom HashMaps.
  • If I have two custom HashMaps of this sort {"key1" : 2, "key3" : 4} and {"key3" : 1} the result of a.putAll(b) should be {"key1" : 2, "key3" : 5}
3 Answers

You don't need a new data structure for this, you don't even need a new class that inherits from HashMap. Instead, use the Map.merge method:

newMap.forEach((k, v) -> oldMap.merge(k, v, Integer::sum));

This code uses Map.forEach to traverse the entries of the new map (the one you'd receive as an argument in putAll) and uses Map.merge (along with Integer::sum) to merge its entries into an already existing map (which I've named oldMap here).

I think this is what you are looking for. I made it so that the key can be any type. If you want, you can remove the generic for the key and just extend HashMap<String, Integer>.

import java.util.HashMap;
import java.util.Map;

public class AddingHashMap<K> extends HashMap<K, Integer> {
    @Override
    public Integer put(K key, Integer value) {
        Integer existingValue = super.get(key);
        if (existingValue == null) {
            existingValue = value;
        } else {
            existingValue = existingValue.intValue() + value.intValue();
        }
        return super.put(key, existingValue);
    }

    @Override
    public void putAll(Map<? extends K, ? extends Integer> m) {
        m.entrySet().forEach(entry -> {
            this.put(entry.getKey(), entry.getValue());
        });
    }
}

Here is it working:

public static void main(String[] argv) {
        AddingHashMap<String> myAddingHashMap = new AddingHashMap<>();
        myAddingHashMap.put("One", 1);
        myAddingHashMap.put("Two", 2);
        myAddingHashMap.put("One", 3);

        myAddingHashMap.entrySet().forEach(entry -> System.out.println(entry.getKey() + " - " + entry.getValue()));
    }

Outputs:

One - 4
Two - 2

Later edit: Keep in mind that this is NOT thread-safe.

I don't think there is a datastructure that does that. The purpose of the datastructure is to store data, not to have logic associated to it. The HashMap can store key-value pairs for you but if you need some more advanced, or specific, logic associated with certain operations, you'll need to add it yourself.

One way is to wrap the map in a class which has this logic. Another might be to implenent the Map interface yourself (which could also use a HashMap internally) though I would not recommend that since changing the behaviour is not a great idea.

A minimal wrapper providing adding functionality:

public class AddingMap {
    private final HashMap<String, Integer> map;

    public AddingMap() {
        map = new HashMap<>();
    }

    public void add(String key, Integer value) {
        map.put(key, map.getOrDefault(key, 0) + value);
    }
    
    public Integer get(String key) {
        return map.get(key);
    }
}

Edit

Shouldn't have finished writing the answer half way...

Indeed, the addAll() method is missing:

public void addAll(Map<String, Integer> map) {
    map.entrySet().forEach(e -> this.add(e.getKey(), e.getValue()));
}
Related