views:

183

answers:

6

I am looking for help on the subject how to use an Interface as Maps Key. I tried to implement a solution, and get no compiletime errors but runtime errors when running my integration tests. Is it not possible to use an Interface as a Key, or is it my tests there is something wrong with?

My code looks something like this

private Map<AInterface, Values> myMap = new HashMap<AInterface, Values>();

Upon retreiving the set of keys from myMap they do contain objects with expected Id, but are compared to not-equal. So when using myMap.get(Object key) i get null, eventhough an object with the same id is there. When using the concrete class instead of the interface all tests pass:

private Map<AClass, Values> myMap = new HashMap<AClass, Values>();

I've read http://java.sun.com/developer/technicalArticles/J2SE/generics/ where it states that for a Map, you are required to replace the type variables K and V by concrete types that are subtypes of Object.

Since the compiler does not give me any warnings when using an Interface for K, my guess would have been that the tests have errors.

Does anybody have any experience with using Interfaces as Key in a Map? And could give me any hints on what I am doing wrong?

+4  A: 

Your classes must implement hashCode and equals (explanation; you should also familiarize yourself with the contract of the Map-interface).

pmf
+2  A: 

The objects that are extending your interface should all implement both hashCode and equals. If equals returns true but the hashCode values are not equal, then the appropriate object is not found since the JVM places the objects in 'buckets' (when storing in a Map) according to their hashCode value.

Wesho
ok, since you are giving me hints on how to fix the runtime errors, i understand it should be possible to use Interfaces as keys in a Map. Is this correct?to your hint, the objects that implements the Interface do inheret an implementation of both hashCode and equals from an Abstract class. The hashCode and equals both use the UUID. The Hashcode is generated from the UUID and the equals method take this into account. How could this be a problem?
Margrethe
You can use an interface when you are declaring the Map instance. There is no problem with that. But obviously you have to pass objects that implement that interface when filling the Map. Are the UUIDs unique per object? Maybe that the objects are meaningfully equal but are not seen as equal since the UUID is unique. e.g. two strings having the value "a" are meaningfully equal but if their equals method is based on UUIDs then they are not seen as so.
Wesho
the equal method is based on UUID, I am not that experienced using UUID and have not implemented the abstract class that implement the equal and hashcode, but did believe they where unique per object. Allthough it seems that this is not the case, since the hashcode is different for objects with the same id (they are meaningfully equal). If I change to using the AClass as key in myMap, the hashCode is equal..
Margrethe
A: 

Both examples should work perfectly. It's OK to use interfaces, abstract or concrete class types in the 'generics'. I often use the List interface in Maps and never had problems.

You say you only have to change the type of myMap and the constructor to make your tests pass? What type of objects do you use as keys in the map, AClass or something else?

Have a close look at your runtime errors or provide us some details (just the exceptions w/o stacktraces).

For the other problem, as Wesho already answered, implement either both hashcode and equals or (for testing) none of them on the class that you really use for keys in the map.

EDIT

Knowing from a comment, that hashcode and equals are implemented: there's one possible trap - if you change the UUID after the object has already been put to the map (as a key), then you may not be able to find your values afterwards (although a rehash on the map should make it work again).

EDIT 2

If you receive a NPE directly on myMap.get(AClass key), then either myMap is null or the key (but that still doesn't solve the other mystery...)

EDIT 3

Just checked the implementation of hashcode and equals on UUID and that's ok. The calculation is limited to the 128 bit UUID only. So if you create two UUID objects for the same UUID value and test for equality, then the two UUID objects are not the same but equal. That's good. If you have a getter for UUID in AClass, then you experiment with a Map like HashMap<UUID, Values> myMap and check if it still works (Maybe, by chance, the code change unhides the real bug ;) )

Andreas_D
Thank you for clearing out that it should be possible to use interfaces, abstract and concrete class types! I now can eliminate this as the solution to my problems :) You say you only have to change the type of myMap and the constructor to make your tests pass?Yes, that's true. changing private Map<AInterface, Values> myMap = new HashMap<AInterface, Values>();to private Map<AClass, Values> myMap = new HashMap<AClass, Values>(); makes all the tests pass instead of all the tests failing. .. break comment... continuing in next comment...
Margrethe
The stacktrace only gives me a nullpointerexception on myMap.get(), the value is null for the given key. I have traversed all the keys and found the key by comparing id, but not found the object by using equals. I belive the hashcode is incorrect. Using toString (which I must admit is not overriden) on the object I get to different strings. Which I guess indicates that there is something wrong with the hashCode. Maybe I shouldn't use UUID when generating the hashCode? Does this change when retrieving objects from the database?
Margrethe
Could you add your implementation of hashcode and equals to your question above? That could help! Ah, and please state, if AClass is the only implementation of AInterface and please check if your have compiler warnings where you use myMap.
Andreas_D
hashCode is generated like this : return getUuid().hashCode();equals is like this : return uuidIdentifiable.getUuid().equals( ((UuidIdentifiable)object).getUuid()); (uuidIdentifiable is the abstract class, AbstractUuidIdentifiable which AClass extends)at time being AClass is the only implementation of AInterfaceThere are no warnings on myMap...
Margrethe
A: 

[...] it states that for a Map, you are required to replace the type variables K and V by concrete types that are subtypes of Object.

I was close to asking a question at SO but I think I've got the answer.

concrete type - It sounds a bit like 'concrete class', as if interfaces or abstract classes are not allowed. But it just says, that your not allowed to replace 'K' by another generic type 'S' or so. It has to be a 'real' type: interfaces, classes, even enums are concrete enough.

subtypes of Object - Again it sounds like interfaces are not allowed because they don't subclass Object. Yes, but: you can't instantiate an interface anyway, so the real objects that are put into the map are always class instances. The only class of Java types that does not subclass Object are Java primitives , including arrays of Java primitives. So Map<int, String> is not allowed as well as Map<String, int[]>.

Andreas_D
Map<String, int[]> works perfectly, as int[] is an Arrays and thus a concrete type internally. Greetz, GHad
GHad
I don't doubt that int[] is somewhat special but arrays don't subclass java.util.Arrays. Otherwise we would see the static methods of Arrays on arrays. But there is no method, just the `length` field. Striked it out for the moment, I have to dig deeper again.
Andreas_D
A: 

The problem is nothing to do with interfaces. The generic types are erased at compilation, and at run-time HashMap only deals with Object instances.

We've no way of knowing what your problem is as you don't show any code, but it's most likely that your keys are mutable and your hashCode is changing.

For instance:

class MyKey {
    String name;

    public int hashCode() {
        return name.hashCode();
    }

    // assume suitable equals() implementation
}

Map<MyKey,Integer> myMap = new HashMap();

MyKey key1 = new MyKey();
key1.name = "Jimmy";

myMap.put(key1, 10);

key1.name = "Johnny";

myMap.get(key1);  // return null

If the hashcode of an object changes between it being added to a map, and trying to retrieve it, then the HashMap will (probably) look in the wrong hash bucket, and not find any value. You might sometimes get a result, if the old hashcode and the new hashcode resolve to the same hash bucket.

David Roussel
A: 

If you're using Apache Commons lang JAR, perhaps add this to your class.

public int hashCode() { 
    return HashCodeBuilder.reflectionHashCode(this);
}
Dean J