问题
I want to create a map that will provide the benefits of generics, whilst supporting multiple different types of values. I consider the following to be the two key advantages of generic collections:
- compile time warnings on putting wrong things into the collection
- no need to cast when getting things out of collections
So what I want is a map:
- which supports multiple value objects,
- checks values put into the map (preferably at compile-time)
- knows what object values are when getting from the map.
The base case, using generics, is:
Map<MyKey, Object> map = new HashMap<MyKey, Object>();
// No type checking on put();
map.put(MyKey.A, "A");
map.put(MyKey.B, 10);
// Need to cast from get();
Object a = map.get(MyKey.A);
String aStr = (String) map.get(MyKey.A);
I've found a way to resolve the second issue, by creating an AbstractKey, which is generified by the class of values associated with this key:
public interface AbstractKey<K> {
}
public enum StringKey implements AbstractKey<String>{
A,B;
}
public enum IntegerKey implements AbstractKey<Integer>{
C,D;
}
I can then create a TypedMap, and override the put() and get() methods:
public class TypedMap extends HashMap<AbstractKey, Object> {
public <K> K put(AbstractKey<K> key, K value) {
return (K) super.put(key, value);
}
public <K> K get(AbstractKey<K> key){
return (K) super.get(key);
}
}
This allows the following:
TypedMap map = new TypedMap();
map.put(StringKey.A, "A");
String a = map.get(StringKey.A);
However, I don't get any compile errors if I put in the wrong value for the key. Instead, I get a runtime ClassCastException
on get().
map.put(StringKey.A, 10); // why doesn't this cause a compile error?
String a = map.get(StringKey.A); // throws a ClassCastException
It would be ideal if this .put() could give a compile error.
As a current second best, I can get the runtime ClassCastException
to be thrown in the put() method.
// adding this method to the AbstractKey interface:
public Class getValueClass();
// for example, in the StringKey enum implementation:
public Class getValueClass(){
return String.class;
}
// and override the put() method in TypedMap:
public <K> K put(AbstractKey<K> key, K value){
Object v = key.getValueClass().cast(value);
return (K) super.put(key, v);
}
Now, the ClassCastException
is thrown when put into the map, as follows. This is preferable, as it allows easier/faster debugging to identify where an incorrect key/value combination has been put into the TypedMap.
map.put(StringKey.A, 10); // now throws a ClassCastException
So, I'd like to know:
- Why doesn't
map.put(StringKey.A, 10)
cause a compile error? How could I adapt this design to get meaningful compile errors on put, where the value is not of the associated generic type of the key?
Is this is a suitable design to achieve what I want (see top)? (Any other thoughts/comments/warnings would also be appreciated...)
Are there alternative designs that I could use to achieve what I want?
EDIT - clarifications:
- If you think this is a bad design - can you explain why?
- I've used String and Integer as example value types - in reality I have a multitude of different Key / value type pairs that I would like to be able to use. I want to use these in a single map - that's the objective.
回答1:
You are messing with generics and overloading in a bad way. You are extending HashMap<AbstractKey, Object>
and so your class is inheriting the method Object put(AbstractKey k, Object v)
. In your class you are defining another put
method with a different signature, which means you are just overloading the put
method, instead of overriding it.
When you write map.put(StringKey.A, 10)
, the compiler tries to find a method that conforms to the argument types put(StringKey, Integer)
. Your method's signature doesn't apply, but the inherited put
's does -- StringKey
is compatible with AbstractKey
and Integer
is compatible with Object
. So it compiles that code as a call to HashMap.put
.
A way to fix this: rename put
to some custom name, like typedPut
.
BTW talking from experience your approach is very fun and engaging, but in real life it just isn't worth the trouble.
回答2:
Item 29: Consider typesafe heterogeneous containers.—Joshua Bloch, Effective Java, Second Edition, Chapter 5: Generics.
回答3:
IMHO, every problem comes from the original design smell: wanting to put values of different types into the map. I would wrap your Integer and String values into a common Value
type instead. Something like this:
public class Value {
private enum Type {
STRING, INTEGER;
}
private Type type;
private Object value;
private Value(Object value, Type type) {
this.value = value;
this.type = type;
}
public static Value fromString(String s) {
return new Value(s, Type.STRING);
}
public static Value fromInteger(Integer i) {
return new Value(i, Type.INTEGER);
}
public Type getType() {
return this.type;
}
public String getStringValue() {
return (String) value;
}
public Integer getIntegerValue() {
return (Integer) value;
}
// equals, hashCode
}
This way, you just need a Map<SomeKey, Value>
, and you can safely get the value from the map:
Value v = map.get(someKey);
if (v.getType() == Type.STRING) {
String s = v.getStringValue();
}
else if (v.getType() == Type.INTEGER) {
Integer i = v.getIntegerValue();
}
回答4:
Consider this: (Thanks to Effective java)
public class TypedMap {
private Map<AbstractKey<?>, Object> map = new HashMap<AbstractKey<?>, Object>();
public <T> T get(AbstractKey<T> key) {
return key.getType().cast(map.get(key));
}
public <T> T put(AbstractKey<T> key, T value) {
return key.getType().cast(map.put(key, key.getType().cast(value)));
}
public static interface AbstractKey<K> {
Class<K> getType();
}
public static enum StringKey implements AbstractKey<String> {
A, B;
public Class<String> getType() {
return String.class;
}
}
public static enum IntegerKey implements AbstractKey<Integer> {
C, D;
public Class<Integer> getType() {
return Integer.class;
}
}
}
This generate compile time error
TypedMap map = new TypedMap();
TypedMap.AbstractKey<Integer> intKey = TypedMap.IntegerKey.C;
TypedMap.AbstractKey<String> strKey = TypedMap.StringKey.A;
map.put(strKey, "A");
map.put(intKey, 10);
map.put(strKey, 10); // this cause a compile error?
来源:https://stackoverflow.com/questions/10429230/map-with-multiple-value-types-with-advantages-of-generics