How to add an entry with an Integer value into Map<String, ?>

4k views Asked by At

I have to place an integer value into the map below.

Map<String,?> map
map.put("key",2000);

When I run the above code I'm getting the following error:

incompatible types: java.lang.Integer cannot be converted to capture#1 of ?
3

There are 3 answers

0
Alexander Ivanchenko On BEST ANSWER

Unbounded generic collections (Collection<?>) are not writable. Let's explore why.

Unknown type - <?>

A question mark in angle brackets <?> is called the unknown type. For the compiler, that means that the actual type can't be predicted and at runtime it could appear to be pretty match anything: an Object, a String, a Cat. Therefore, everything that you retrieve from the unbounded collection will be treated as an Object:

Collection<?> items = new ArrayList<>();
Object item = items.iterator().next(); // this assignment is safe and will compile

But don't confuse an unbounded collection Collection<?> with a collection of objects Collection<Object>. They are incompatible.

Collection<Object> has invariant behavior, i.e. it expects only another collection of type Object to be assigned to it. Meanwhile, we can assign a collection of any type to Collection<?>.

Collection<?> items = new ArrayList<String>();         // OK

Collection<Object> objects1 = items;                   // that will not compile
Collection<Object> objects2 = new ArrayList<String>(); // that will not compile
Collection<Object> objects3 = new ArrayList<Object>(); // OK - compiler will not complain here

So, unknown type could be represented by anything that extends Object. And you might think about Collection<?> as if it is a upper-bounded collection Collection<? extends Object>. They are absolutely compatible.

Collection<?> items = new ArrayList<>();
Collection<? extends Object> upperBounded = items; // no issues
items = upperBounded;                              // no issues

Modifying an unbounded collection

Wild-cards like <?> or <? extends Object> are not actual types. They are just a way to tell the compiler that we don't know what the type will be at runtime.

Then what will be the actual type of Collection<?> ?

Since it's already clear that unknown type (<?>) implies that it could be any type that extends Object, hence under the hood of a Collection<?> could appear an ArrayList<Object>, an ArrayList<String>, a HashSet<BigDecimal>, etc.

Remainder: parameterized collection that will appear in the guise of Collection<?> at runtime is invariant, which means that ArrayList<String> is expected to contain only Strings and HashSet<BigDecimal> is expected to store only instances of BigDecimal. But that there's no way to ensure that at runtime because during the compilation generic parameters get erased. Therefore, the only way to provide type-safety while utilizing unbounded generic collection is to disallow to add anything into it.

For example, since it's illegal to add an Integer or an Object into an ArrayList<String>, or to add an instance of String into a HashSet<BigDecimal> compiler will prevent any attempt to add anything to the Collection<?>. Because there's no type compatible with the unknown type.

There's only one exception, null is a valid value for any type of object, and therefore it could be added into an unbounded as well as into an upper-bounded collection.

Collection<?> items = new ArrayList<>();
items.add(null);
System.out.println(items);
items.remove(null);
System.out.println(items.isEmpty());

Will give an output:

[null]  -  null-element was successfuly added
true    -  i.e. isEmpty

Note: that there are no restrictions on removal of elements for both unbounded and upper-bounded collections.

Alternatives

Hence, there's no way to add new entries to the unbounded map you can either copy its entries one by one performing an instanceOf check into the regular generic map Map<String, Integer> get rid of generics at all by assign the map to a variable of row type, and then add type-casts manually when need to retrieve a value like in Java 1.4.

That how you can copy the contents of unbounded map to a regular and utilize the advantages of generics.

Map<String, ?> unboundedMap = new HashMap<>()

Map<String, Integer> writableMap = new HashMap<>();

for (Map.Entry<String, ?> entry: unboundedMap.entrySet()) {
    if (entry.getValue() instanceof Integer) {
        writableMap.put(entry.getKey(), (Integer) entry.getValue());
    }
}

The same thing by using the Stream API

Map<String, Integer> writableMap =
       unboundedMap.entrySet().stream()
                   .filter(entry -> entry.getValue() instanceof Integer)
                   .collect(Collectors.toMap(Map.Entry::getKey,
                                             entry -> (Integer) entry.getValue()));
1
Zikria Azimi On
        Map<String, Integer> map = new Map<String, Integer>;
        map.put("key",2000);
0
rzwitserloot On

You can assign many things to a a variable of type Map<String, ?> . For example:

Map<String, ?> map;

map = new HashMap<String, Object>(); // works
map = new HashMap<String, String>(); // works
map = new HashMap<String, Number>(); // works
map = new HashMap<String, Integer>(); // works

That all works. So, let's say that map = new HashMap<String, String>() was assigned to it. And then you attempt to run .put("key", 2000); on it.

That, obviously, makes no sense. Hence why that code doesn't compile.

I'm assuming that your eyeballs and human brain looked at this code and determined that the map you have there couldn't possibly be pointing at anything other than either a Map<String, Object> or a Map<String, Number> or a Map<String, Integer> or even a Map<String, Serializable> - types for which .put("key", 2000) would be sensible.

But that's not how java works. Part of the point of types is to restrict yourself and give yourself the freedom to assign other stuff later.

It is exactly analogous to this:

Object x = "hello";
x.toLowerCase();

The above does not compile: The Object type doesn't have a toLowerCase() method. Yes, yes, our eyeballs and brain look at this and go: That's stupid, of course it does! Look, x is clearly pointing at a string!

But in such cases, just.. write String x = "hello".

So, the same applies for your scenario. Just write Map<String, Integer> instead.

Unfortunately....

sometimes you have dynamic code that wants to check what a thing is and act accordingly. For example:

void foo(Object o) {
  if (o instanceof String s) return s.toLowerCase();
  throw new SomeException();
}

But you can't do that for generics - generics cannot be instanceof checked. You can't do something like: IF the object my map variable is pointing at has Integer as value type (or any supertype of Integer), then do map.put("key", 2000), otherwise don't.

That's just not how generics works in java. There is simply no way to do this.

The only thing you can do is just decree it. Write code that works fine if it is true, but messes everything up if it is not true: You can FORCIBLY run .put("key", 2000) on the map, which will put that in the map, and if that is a map declared as, say, Map<String, String>, then any code that interacts with this map will start throwing ClassCastExceptions left and right even though they never cast anything. Because you shoved an integer into a map of strings.

Hence why the compiler yells at when you force this, and why you shouldn't be doing this. The only point of generics is to make your programming life easier, to clarify APIs, and to catch bugs, and you're.. doing the opposite of it by forcing it. But, hey, if you really want to do this (highly unlikely):

void example(Map<String, ?> map) {
  Map forceIt = map; // raw assignment
  forceIt.put("key", 2000); // compiles
}

The above compiles and does what you asked for - but will warn you that this is a very bad idea.

of (o instanceof String) return ((String) o).toLowerCase();