Search code examples
javagenericscollectionsunbounded-wildcard

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


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 ?

Solution

  • 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()));