When should I create a class for a Map key?
I'm using Java 6.
Suppose I have a class which I would like to save its instances into a map. Later on I would like to retrieve instances using only the "key fields". I'll ignore field modifiers, getters, and setters for conciseness.
class A {
String field1;
String field2;
String field3;
String field4;
//more fields
public int hashCode(){
//uses only field1 and field2
}
public boolean equals(Object o){
//uses only field1 and field2
}
}
Since Java's standard API doesn't have the MultikeyMap and I don't want to use 3rd party libraries, I have a choice of
1) creating a new classKeyA
to represent the key of a map
2) use A
itself as the key and开发者_JAVA百科 populate only the "key fields" when I need to retrieve objects from a map
3) nest the maps, e.g. HashMap<String, HashMap<String, A>>
4) other workarounds
What do people normally use and when?
Given your recent edit, you should be fine to use instances of class A as keys in this situation. Lookups will be done based on the semantics of equals()
and hashCode()
, so this will cause instances to be retrieved by only the "key fields". Hence the following code would work as you intend:
final Map<A, String> map = new HashMap<A, Object>();
final A first = new A("fe", "fi", "fo", "fum");
map.put(first, "success");
// later on
final A second = new A ("fe", "fi", "foo", "bar");
System.out.println(map.get(second)); // prints "success";
Having said that, your description of option 2 makes me a little concerned that this might not be the most sensible option. If you create a Map<A, String>
, that's a mapping from instances of class A to strings. Yet your second point implies that you want to think of it as a mapping from pairs of key fields to strings. If you're going to usually look up values based on a couple of "raw" strings, then I'd advise against this. It feels wrong (to me), to create a "fake" instance of A just to do a lookup - so in this case, you probably should create a key class that embodies the pair of strings as described in option 1. (You could even embed instances of these within your A
objects to hold the key fields).
There's a similar argument for or against option 3, too. If the strings really are conceptually hierarchical, then it might well make sense. For example, if field1
was Country, and field2
was Town, one could definitely argue that the nested maps make sense - you have a mapping from country, to the map of Town->A relations within that country. But if your keys don't naturally compose in this fashion (say, if they were (x, y) coordinates), this would again not be a very natural way to represent the data, and a single-level map from XYPoint
to value would be more sensible. (Likewise, if you never use the two-level map except to always go straight through both layers, one could argue the one-level map still makes more sense.)
And finally, as for option 4 -if you're always mapping to A
itself, and storing the key as its own value (e.g. if you want to canonicalise your A
instances, a bit like String.intern()
) then as was pointed out you needn't use a Map
at all, and a Set
will do the job here. The Map
is useful when you want to establish relationships between different objects, whereas a Set
automatically gives you the uniqueness of objects without any extra conceptual overhead.
If you do use the class itself as a key, be warned though that objects should only generally be used as keys if their hashCode
(and the behaviour of equals
) won't change over time. Typically this means the keys are immutable, though here you could afford to have mutable "non-key" fields. If you were to break this rule, you'd see odd behaviour such as the following:
// Populate a map, with an A as the key
final Map<A, String> map = new HashMap<A, Object>();
final A a = new A("one", "two", "three", "four");
map.put(a, "here");
// Mutate a
a.setField1("un");
// Now look up what we associated with it
System.out.println(map.get(a)); // prints "null" - huh?
System.out.println(map.containsKey(a)); // prints "false"
I'd create an Index
class, something like this (warning: untested code), to abstract out the indexing functionality. Why Java doesn't have something like this already is puzzling to me.
interface Indexer<T, K>
{
/** extract key from index */
public K getIndexKey(T object);
}
class Index<T,K>
{
final private HashMap<K,List<T>> indexMap = new HashMap<K,List<T>>();
final private Indexer<T,K> indexer;
public Index(Indexer<T,K> indexer)
{
this.indexer = indexer;
}
public void add(T object) {
K key = this.indexer.getIndexKey(object);
List<T> values = this.indexMap.get(key);
if (values == null)
{
values = new ArrayList<T>();
this.indexMap.put(key, values);
}
values.add(object);
}
public void remove(T object) {
K key = this.indexer.getIndexKey(object);
List<T> values = this.indexMap.get(key);
if (values != null)
{
values.remove(object);
}
}
public List<T> lookup(K key) {
List<T> values = this.indexMap.get(key);
return values == null
? Collections.emptyList()
: Collections.unmodifiableList(values);
}
}
example relevant to your class A
:
Index<A,String> index1 = new Index<A,String>(new Indexer<A,String>() {
@Override public String getIndexKey(A object)
{
return object.field1;
}
});
Index<A,String> index2 = new Index<A,String>(new Indexer<A,String>() {
@Override public String getIndexKey(A object)
{
return object.field2;
}
});
/* repeat for all desired fields */
You would manually have to add and remove entries from the indices, but all the grungework below those operations is handled by the Index class.
Your class has "key fields". I would suggest to create a parent class, ParentA
, with those key fields (which certainly map to a concept in your domain) and inherit this class in your child class A
.
Override hashCode()
and equals()
in the ParentA
class.
Use a Map<ParentA, A>
to store your A
instances and give the instance as key and value.
To retrieve a specific A
instance, create a new ParentA
instance, pA
, with your key fields set, and do
A a = map.get(pA);
That's it.
Another way is to create a AIdentifier
class with key fields and add an instance as A
property id
. So you add your instance with map.put(a.id, a);
That's inheritance vs composition pattern discussion :)
精彩评论