简体   繁体   中英

Java generics for a map value

I'd like to directly cast to the appropriate list type in the following scenario. If erasure kills the class type info, what would be the best way to maintain the class type in the following scenario?

class MyProperties {

    private final Map<String, List<?>> properties = new HashMap<>();

    public void put(final String key, final List<?> value) {
        properties.put(key, value);
    }

    public <T> List<T> get(final String key, final Class<T> t) {
        final List<?> object = properties.get(key);
        // Here is where the issue lies because the class type is gone, so we
        // will always skip this block and return null
        if (object.getClass().isAssignableFrom(t)){
            return (List<T>)object;
        }
        return null;
    }

    public static void main(String[] args){
        List<String> myList = new ArrayList<>();
        myList.add("Bacon");
        myList.add("Eggs");

        List<Integer> myList2 = new ArrayList<>();
        myList2.add(1);
        myList2.add(2);

        MyProperties props = new MyProperties();
        props.put("myKey", myList);
        props.put("myKey2", myList2);

        // I'd like to directly cast to the appropriate list type if possible
        List<String> foo = props.get("myKey", String.class);
        List<Integer> foo2 = props.get("myKey2", Integer.class);
    }    
}

Storing the element type

What you could do is pass the class of the list elements as well and store it separately:

private final Map<String, List<?>> properties = new HashMap<>();
private final Map<String, Class<?>> propClasses= new HashMap<>();

public <T> void put(final String key, final List<T> value, final Class<T> elementClass) {
  properties.put(key, value);
  propClasses.put(key, elementClass);
}

As you already noted, due to type erasure you don't know the type of the list elements. Thus you need to store that class separately, hence a second map.

To ensure that the correct class is passed, you change the put method to take a class parameter which must be of the same type as the generic type of the list. However, please note that you are still able to disable type checking by using raw types.

Example:

//works because the type is Integer for the list and the class
put( "working", new ArrayList<Integer>(), Integer.class ); 

//doesn't work since Number != Integer, even though Integer extends Number
put( "error", new ArrayList<Number>(), Integer.class );
put( "still an error", new ArrayList<Integer>(), Number.class );

//careful, this compiles because the raw type List is used
//the compiler would warn us though, so don't take those warnings lightly :)
put( "raw", (List)new ArrayList<Number>(), Integer.class );

Retrieving the lists with exact element type

public <T> List<T> get(final String key, final Class<T> t) {
  Class<?> propClass = propClasses.get( key );                  
  if( propClass == null || t == null || !t.equals( propClass )  ) {
    return null;
  }

  return (List<T>)properties.get(key);        
}

When getting elements you first get the class in the according map and check whether t is the class itself or a super class. If this succeeds you get the list from the properties map and cast it to the type you need.

t.equals( propClass ) is used, since you should not cast the list to a super type of the elements. Otherwise you'd be able to insert elements of the wrong type.

As an example assume List<Integer> . The following would cause problems:

//assume this were allowed by your code 
List<Number> l = get("working", Number.class ); 
l.add( new Double( 0.5 ) ); //ouch, now we have a double in an integer list 

Retrieving the lists with element type as lower bound (read-only)

If you want to use a super class of the element class, you could change the get() method or add a "read-only" method as follows:

public <T> List<? extends T> getReadOnly(final String key, final Class<T> t) {
  Class<?> propClass = propClasses.get( key );          
  if( propClass == null || t == null || !t.isAssignableFrom( propClass ) ) {
    return null;
  }

  return (List<? extends T>)properties.get(key);        
}

If T is the element class or a super class, we allow casting to List<? extends T> List<? extends T> . Thus the compiler allows us to access the elements using a super type but wouldn't allow us to add new elements - unless we break it by manual casts.

Example:

List<? extends Number> l = getReadOnly( "working", Number.class );

//won't compile since the compiler isn't sure about the type of the list
//in our case it would actually be a List<Integer> but the compiler doesn't know that
//and thus disallows adding to the list - which is good since we'd produce a bug here
l.add( new Double(0.5));

//this would compile due to the raw type, which essentially disables type checks here
//fortunately the compiler warns us not to do this or at least be very careful
((List)l).add( new Double(0.5));

You are trying to enforce type integrity using generic method signature. But it obviously will be erased after compilation (type erasure).

So you will need to actually store the type as an object. This way your type information will not be lost after compilation.

Since you need to store one type per key, obvious choice of data structure would be additional Map<String, Class<?>>

I guess you can easily link the stored type with the new method signature.

Hope this helps.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM