Wednesday, February 13, 2008

A Safe HashMap for Java

If I was forced to specify what I loved most about Ruby my first answer would have to be terseness. Consider the case where you want to iterate through a list of objects and generate a hashmap of arrays:
h = Hash.new

for value in list
# if the key 'foo' contains nil, populate it with an Array,
# then add new value to array under key 'foo'
(h[value.foo] ||= Array.new) << value.bar
end
That was expanded for the sake of readability, I would normally write it like this:
h = {}
list.each { |v| (h[v.foo] ||= []) << v.bar }
but I digress.

Oh, the horror that arose when I worked on a Java project again, only to run into a similar requirement - verbose beast!
HashMap<String, List<String>> h = new HashMap<String, List<String>>();

for(Thing v : list) {
List<String> ar = h.get(v.foo);
if(ar == null) {
ar = new ArrayList<String>();
h.put(v.foo, ar);
}
ar.add(v.bar);
}
I'm sure it looks familiar to most who work with Java.

So I created a safe hashmap for Java, which encapsulates the above behavior. Now the code is only this:
SafeHashMap<String, List<String>> h = new SafeHashMap<String, List<String>>(new ArrayList<String>());

for(Thing v : list) {
h.get(v.foo).add(v.bar);
}
Notice the addition of the object passed into the constructor. This object is cloned and returned if the Map contains no value for the given key.
import java.util.HashMap;

public class SafeHashMap<K,V> extends HashMap<K,V>
{
private V type;

public <T extends V> SafeHashMap(int initialCapacity, float loadFactor, T type) {
super(initialCapacity, loadFactor);
this.type = type;
}

public <T extends V> SafeHashMap(int initialCapacity, T type) {
super(initialCapacity);
this.type = type;
}

public <T extends V> SafeHashMap(T type) {
super();
this.type = type;
}

public V get(Object key) {
V value = super.get(key);
if(value == null) {
try {
value = (V)this.type.getClass().getMethod("clone").invoke(type);
super.put((K)key, value);
} catch(Exception e) {
e.printStackTrace();
}
}
return value;
}
}

There's my quota: one Java submission per quarter.

8 comments:

Adam said...

This is similar to existing code in Jakarta Commons Collections. Specifically, the LazyMap and the MultiValueMap classes. The only difference is this version is generic compatible.

Google Collections (http://code.google.com/p/google-collections/) offers a Generics-enabled multimap already.

Eric said...

MultiValueMap is specifically for Lists - but this lets you nest any arbitrary values. But true, this is similar to LazyMap - just more concise.

That said, I was not aware of Google Collections - looks cool. The benefit of my method, however, is it is generic AND has the power of a concise LazyMap -arbitrary values.

Thanks for the heads up!

dibblego said...

You're looking for the nullable type monad. Effectively binding through a partial function (i.e. nullable type).

Imagine a type that could be null A. We might encode its possible values as a type Option[A] with possible construction values of:

Some[A](A a) // not null
None // is null

Then, bind through this type would be:

bind(f, None) => None
bind(f, Some(a)) => f(a)

We have this in decent languages. You might consider upgrading from Java.

Signed, a former member of the Java development team, Sun Certified Programmer and Developer.

PS: Java is rubbish

Dhananjay Nene said...

Nice post. Blogged an enhancement here.

http://blog.dhananjaynene.com/archives/20

Eric said...

dibblego -
Seriously? You obviously don't know me, or my feelings on null.

dhananjay -
Nice. You're right about the Array thing... stupid Java :(
I just wanted to avoid the creation of a factory, like LazyMap requires.

JAlexoid said...

[sarcasm]
And if you subclass Ruby classes and make all the methods on letter long, think of the space you could save!
[/sarcasm]

BTW: Comparing apples to oranges, is not really productive...(oranges=Ruby,apples=Java, criteria=static_vs_dynamic)

Peter Lawrey said...

You could use an anonymous class as well.

Map>String, List>String>> h = new HashMap>String, List>String>>() {
public List>String> get(Object text) {
List>String> ret = super.get(text);
if(ret == null) put(text, ret = new ArrayList>String>());
return ret;
}
}

but I have a simple abstract factory which returns a new value for each key once. and other variations...

Eric said...

jalexoid -

In a similar vein: Unproductive comments are - by definition - not productive. Simply declaring, by fiat, that comparing apples to oranges is not productive is just plain silly. Sometime comparing them is not meaningless juxtapose, but instead breeds interesting and new insights. Lighten up.

peter -

Yes, some people have suggested that. If I didn't have a rabid distaste for factories I would not have avoided that road. Though, since we are talking about Java here, that's the closest thing to closures we're likely to get ATM - so I like it!