SIGNALW

Implement nested Map with default value as empty Map in Groovy

Groovy does not have support for nested Map with default value out of the box. So in order to insert values in a nested Map, where the ancestor keys do not exist yet, you have to initialize them first. Otherwise, you will get a NullPointerException, e.g.:

def m = [:]
m['a']['b']['c'] = 1
//java.lang.NullPointerException: Cannot get property 'b' on null object
//  at Script1.run(Script1.groovy:2)

Here, I find the workarounds below can be useful if you don’t want to do the initialization or don’t want to worry about key existance.

Method 1: withDefault method

If you already know how deep your nested Map will be, you can directly use multiple withDefault methods and put an empty Map in closure.

def m = [:].withDefault { [:].withDefault { [:] } }
m['a']['b']['c'] = 1
​assert m == [a: [b: [c: 1]]]

// enhance withDefault method (or create a new one)
LinkedHashMap.metaClass.withDefault = { int i ->
  if (i <= 0) return [:]
  return MapWithDefault.newInstance([:], { delegate.withDefault(i - 1) })
}

def m = [:].withDefault(3)
m['a']['b']['c']['d'] = 1
assert m == [a: [b: [c: [d: 1]]]]

This of course is not ideal if you want the Map to go arbitrarily deep

Method 2: Modify getAt method on LinkedHashMap MetaClass

This approach allows you to go as deep as you want, but the limitation is that all of your keys have to be either String or non-String. Also remember that the dot notation won’t work.

// If you know your Map keys are always String:
LinkedHashMap.getMetaClass().getAt = { String k ->
  if (!delegate.containsKey(k)) {
    delegate.put(k, [:])
  }
  delegate.get(k)
}

def m = [:]
m['a']['b']['c'] = 1
assert m == [a: [b: [c: 1]]]

// If you know your Map keys do not contain String:
LinkedHashMap.getMetaClass().getAt = { k ->
  if (!delegate.containsKey(k)) {
    delegate.put(k, [:])
  }
  delegate.get(k)
}

def m = [:]
m[1][2][3.3] = 'a'
assert m == [1:[2:[3.3:'a']]]

Method 3: Create a custom class that extends LinkedHashMap, then override get method

You can create a new class in exchange for some flexibility.

class DefaultMap extends LinkedHashMap {
    def get(k) {
        if (!super.containsKey(k)) {
            super.put(k, new DefaultMap())
        }
        super.get(k)
    }
}

def m = new DefaultMap()
m['a']['b']['c'] = 1
assert m == [a: [b: [c: 1]]]

Method 4: Add a new method on LinkedHashMap MetaClass

This works fine if you don’t need subscript operator or dot notation.

LinkedHashMap.metaClass.customGet = { k ->
  if (!delegate.containsKey(k)) {
    delegate.put(k, [:])
  }
  delegate.get(k)
}


def m = [:]
m.customGet('a').customGet('b').put('c', 1)
assert m == [a: [b: [c: 1]]]

Any other methods you can think of? Which method do you prefer?