5

It may be a bad practice, but I haven't been able to figure out any better solution for my problem. So I have this map

// Map<state, Map<transition, Map<property, value>>>
private Map<String, Map<String, Map<String, String>>> properties;

and I want to initialize it so I don't get NullPointerException with this

properties.get("a").get("b").get("c");

I tried this one but I didn't work (obviously)

properties = new HashMap<String, Map<String, Map<String,String>>>();

Other things I tried didn't compile.

Also if you have any ideas how to avoid this nested maps, I would appreciate it.

Sergey Grinev
  • 34,078
  • 10
  • 128
  • 141
user219882
  • 15,274
  • 23
  • 93
  • 138
  • *"It may be a bad practice, but I haven't been able to figure out any better solution for my problem."* You are right. It is almost certainly bad practice. If you wrote a new question outlining the problem (specifically the requirements of the data structure), someone may be able to suggest that better solution that you can't figure out. – Stephen C Jan 30 '12 at 12:40

9 Answers9

9

It seems to me that you need to create your own Key class:

public class Key {
   private final String a;
   private final String b;
   private final String c;
   public Key(String a, String b, String c) {
      // initialize all fields here
   }

   // you need to implement equals and hashcode. Eclipse and IntelliJ can do that for you
}

If you implement your own key class, your map will look like this:

Map<Key, String> map = new HashMap<Key, String>();

And when looking for something in the map you can use:

map.get(new Key("a", "b", "c"));

The method above will not throw a NullPointerException.

Please remember that for this solution to work, you need to override equals and hashcode in the Key class. There is help here. If you don't override equals and hashcode, then a new key with the same elements won't match an existing key in the map.

There are other possible solutions but implementing your own key is a pretty clean one in my opinion. If you don't want to use the constructor you can initialize your key with a static method and use something like:

Key.build(a, b, c)

It is up to you.

Community
  • 1
  • 1
Ravi Wallau
  • 10,416
  • 2
  • 25
  • 34
  • 1
    +1. I've found that having generic Pair and Triple container classes (for holding two or three objects, respectively) is useful for this purpose, amongst many others... – ach Nov 09 '12 at 20:05
5

You need to put maps in your maps in your map. Literally:

properties = new HashMap<String, Map<String, Map<String,String>>>();
properties.put("a", new HashMap<String, Map<String,String>>());
properites.get("a").put("b", new HashMap<String,String>());

If your target is lazy initialization without NPE you have to create your own map:

private static abstract class MyMap<K, V> extends HashMap<K, V> {
    @Override
    public V get(Object key) {
        V val = super.get(key);
        if (val == null && key instanceof K) {
            put((K)key, val = create());
        }
        return val;
    }

    protected abstract V create();
}


public void initialize() {
    properties = new MyMap<String, Map<String, Map<String, String>>>() {
        @Override
        protected Map<String, Map<String, String>> create() {
            return new MyMap<String, Map<String, String>>() {
                @Override
                protected Map<String, String> create() {
                    return new HashMap<String, String>();
                }
            };
        }
    };

}
Sergey Grinev
  • 34,078
  • 10
  • 128
  • 141
  • But I don't want to put any values in there in the beginning. So this means that the only solution here is to check for null during access and if so, create the next level? – user219882 Jan 30 '12 at 12:14
  • Guava has a `CacheBuilder` that can create a map with lazy initialization. – krlmlr Oct 27 '12 at 00:27
5

You could use a utility method:

  public static <T> T get(Map<?, ?> properties, Object... keys) {
    Map<?, ?> nestedMap = properties;
    for (int i = 0; i < keys.length; i++) {
      if (i == keys.length - 1) {
        @SuppressWarnings("unchecked")
        T value = (T) nestedMap.get(keys[i]);
        return value;
      } else {
        nestedMap = (Map<?, ?>) nestedMap.get(keys[i]);
        if(nestedMap == null) {
          return null;
        }
      }
    }
    return null;
  }

This can be invoked like this:

String result = get(properties, "a", "b", "c");

Note that care is required when using this as it is not type-safe.

McDowell
  • 107,573
  • 31
  • 204
  • 267
1

The only way to do it with this structure is to pre-initialise the 1st and 2nd level maps with ALL possible keys. If this is not possible to do you can't achieve what you are asking with plain Maps.

As an alternative you can build a custom data structure that is more forgiving. For example a common trick is for a failed key lookup to return an "empty" structure rather than null, allowing nested access.

Mike Q
  • 22,839
  • 20
  • 87
  • 129
1

You can't initialize this in one go, since you normally don't know what keys you'll have in advance.

Thus you'd have to check whether the submap for a key is null and if so you might add an empty map for that. Preferably you'd only do that when adding entries to the map and upon retrieving entries you return null if one of the submaps in the path doesn't exist. You could wrap that in your own map implementation for ease of use.

As an alternative, apache commons collections' MultiKeyMap might provide what you want.

Thomas
  • 87,414
  • 12
  • 119
  • 157
1

It's impossible to use properties.get("a").get("b").get("c"); and be sure to avoid null unless you make your own Map. In fact, you can't predict that your map will contains "b" key. So try to make your own class to handle nested get.

alain.janinm
  • 19,951
  • 10
  • 65
  • 112
1

I think a better solution is using an object as the only key to the map of values. The key will be composed of three fields, state, transition and property.

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

public class Key {

    private String state;

    private String transition;

    private String property;

    public Key(String state, String transition, String property) {
        this.state = state;
        this.transition = transition;
        this.property = property;
    }

    @Override
    public boolean equals(Object other) {
        return EqualsBuilder.reflectionEquals(this, other);
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this);
    }

}

When you check for a value, the map will return null for a key that is not associated with a value

Map<Key, String> values = new HashMap<Key, String>();
assert values.get(new Key("a", "b", "c")) == null;

values.put(new Key("a", "b", "c"), "value");
assert values.get(new Key("a", "b", "c")) != null;
assert values.get(new Key("a", "b", "c")).equals("value");

To efficiently and correctly use an object as a key in a Map you should override the methods equals() and hashCode(). I have built thos methods using the reflective functionalities of the Commons Lang library.

frm
  • 3,346
  • 1
  • 21
  • 21
  • Answer is OK, but needs a proper (not reflection-based) equals() & hashCode() implementation. Reflection-based ones are neither performant -- for a map key, this is important -- nor do they show people the basic principles to do it correctly. – Thomas W Feb 11 '14 at 19:59
0

I think, following is the easier way:

public static final Map<Integer, Map<Integer, Map<Integer, Double>>> A_Map = new HashMap<Integer, Map<Integer, Map<Integer, Double>>>()
{
    {
        put(0, new HashMap<Integer, Map<Integer, Double>>()
        {
            {
                put(0, new HashMap<Integer, Double>()
                {
                    {
                        put(0, 1 / 60.0);
                        put(1, 1 / 3600.0);
                    }
                });

                put(1, new HashMap<Integer, Double>()
                {
                    {
                        put(0, 1 / 160.0);
                        put(1, 1 / 13600.0);
                    }
                });
            }
        });

        put(1, new HashMap<Integer, Map<Integer, Double>>()
        {
            {
                put(0, new HashMap<Integer, Double>()
                {
                    {
                        put(0, 1 / 260.0);
                        put(1, 1 / 3600.0);
                    }
                });

                put(1, new HashMap<Integer, Double>()
                {
                    {
                        put(0, 1 / 560.0);
                        put(1, 1 / 1300.0);
                    }
                });
            }
        });
    }
};
TuneFanta
  • 157
  • 2
  • 10
0

Using computeIfAbsent/putIfAbsent makes it simple:


private <T> void addValueToMap(String keyA, String keyB, String keyC, String value) {
    map.computeIfAbsent(keyA, k -> new HashMap<>())
        .computeIfAbsent(keyB, k -> new HashMap<>())
        .putIfAbsent(keyC, value);
}

nogape
  • 1