Reputation: 1375
In the application I'm developing, I make use of several maps associating strings to collections of elements, e.g. Map<String, List<String>>
, Map<String, SortedSet<Object>>
. In many cases I want simple functions to add/remove elements in the collection given by a certain key, possibly removing or creating new entries in the map.
I implemented some generic methods for the effect, but the putIntoCollection()
method is giving me some problems. My implementation, which does not rise any warning, is the following:
public static <K, V, C extends Collection<V>> void putIntoCollection(
Map<K, C> map, K key, V value, Class<? extends C> collectionClass)
throws InstantiationException, IllegalAccessException {
C collection = map.get(key);
if (collection == null) {
collection = collectionClass.newInstance();
map.put(key, collection);
}
collection.add(value);
}
C
represents the type of the collection, which can be any type of Collection
, and the Class<? extends C>
parameter allows me to pass a concrete class token to instantiate new C
's (e.g. pass a token of ArrayList
for a Map of Lists
).
However, if I try to use it like this:
Map<String, Set<String>> tags;
String key, value;
MapUtilities.putIntoCollection(map, key, value, HashSet.class);
I get the compile error:
The parameterized method <K, V, Set<V>>putIntoCollection(Map<K,Set<V>>, K, V, Class<? extends Set<V>>) of type MapUtilities is not applicable for the arguments (Map<K,Set<V>>, K, V, Class<HashSet>)
I understand that happens because I'm passing a Class<HashSet>
argument while it expects a parametrized Set class. However, I don't know how (or if) can I obtain such instances of Class
. Is there a better way to do a generic method such as this?
Upvotes: 3
Views: 3534
Reputation: 2407
Your approach can't work because Class<List>
is not a sub-class of Class<Collection>
. So you won't be able to use an instance of Class
or any other container that carries the type of interest (? extends C
) as a generic type. You can only use C
as a direct parameter, like a kind of prototype.
Look at <T> T[] java.util.List.toArray(T[] a)
as an example. This method also just wants to know the type T
of the resulting array. It can't be delivered as Class<T>
so the method wants the client to give an instance.
Upvotes: 0
Reputation: 236004
After fiddling a bit with generics, this is the best (working!) solution I could find:
@SuppressWarnings("unchecked")
public static <K, V, C extends Collection<V>> void putIntoCollection(
Map<K, C> map, K key, V value, Class<?> collectionClass)
throws InstantiationException, IllegalAccessException {
C collection = map.get(key);
if (collection == null) {
collection = (C) collectionClass.newInstance();
map.put(key, collection);
}
collection.add(value);
}
The unchecked
warning is unavoidable, as is the Class<?>
parameter and the (C)
cast. The trouble you're having with this method boils down to type erasure - at runtime you can't accurately specify the generic type of the collections in the map, because this information only exists at compile time and is lost during program execution.
Now this will work without problems:
Map<String, Set<String>> map = new HashMap<String, Set<String>>();
String key="x", value="y";
putIntoCollection(map, key, value, HashSet.class);
Be aware, though, that this will also work without compilation errors:
putIntoCollection(map, key, value, Vector.class);
In its current form, there's no way to specify in the method that the collection values in the map (of type Set<String>
) are of the same type as the collection values instantiated inside the method (HashSet
in the first example, which is correct and Vector
in the second example, which is incorrect). Again that's because of type erasure, at compile time both collection instances work fine (a HashSet
and a Vector
) because both implement Collection
and contain elements of type String
, but at runtime this line will work for the first example but will fail for the second example with a ClassCastException
:
Set<String> set = map.get(key);
Upvotes: 1
Reputation: 198033
Are you in a position to use third-party libraries? You're basically reinventing Guava's Multimap
-- ListMultimap<String, String>
and SortedSetMultimap<String, Object>
are your two examples. A bunch of implementations are provided -- most notably, for your case, ArrayListMultimap
and TreeMultimap
.
That said, it's generally easier to just pass an explicit factory object:
interface Supplier<T> {
T get();
}
void putIntoCollection(Map<K, Set<V>>, K, V, Supplier<Set<V>> emptySetSupplier);
Upvotes: 2
Reputation: 12692
That's a problem related to generic type reification... I think Guice uses a TypeLiteral to get around this problem.
But I'm going to sidestep your question. It seems like what you really want is Guava's Multimap. It's a collection similar to a Map, but which may associate multiple values with a single key. It also offers utility methods such as the one you were looking for.
You can find a detailed explanation in the Guava wiki: http://code.google.com/p/guava-libraries/wiki/NewCollectionTypesExplained#Multimap
Upvotes: 1
Reputation: 359786
This seems rather over-engineered to me. Have you considered using Guava's Multimaps instead?
http://code.google.com/p/guava-libraries/wiki/CollectionUtilitiesExplained#Multimaps
Upvotes: 0