2

This is a contrived example to illustrate the problem. I know there are ways around this that don't generate compiler warnings and you can also disable the warning. I'm wondering if this is possible without any of those tricks.

Given this code:

1 public static void main(String[] args) {
2    Map<String,String> map = null;
3
4    HashMap<String,String> hmap;
5
6    if(map instanceof HashMap)
7        hmap = (HashMap<String,String>)map;
8    else
9        hmap = new HashMap<String,String>(map);
10
11   map = (Map<String,String>)hmap.clone();
12
13    Object o = hmap.clone();
14    if(o instanceof Map<?,?>)
15        map = (Map<String,String>)o;
16 }

The code on both lines 11 and 15 generate the compiler warning:

Unchecked cast from Object to Map<String,String>

Line 11 is a little understandable: Object.clone() returns an Object, and there has been no instanceof check prior to casting. The programmer knows that the clone will be a Map<String,String>, but the compiler can't prove it.

Line 15 is puzzling to me, though. Usually, checking the type of a variable using instanceof and then immediately casting it will not generate such a warning. Indeed, replacing the code with non-parameterized classes like this will generate no warnings, on either of these lines of code:

static class A {}
static class B extends A implements Cloneable {
    public Object clone() { return null; }
}
public static void main(String[] args) {
    A a = null;

    B b;

    if(a instanceof B)
        b = (B)a;
    else
        b = new B();

    a = (A)b.clone();

    Object o = b.clone();
    if(o instanceof A)
        a = (A)o;
}

Back to the original code (with the Map<String,String> references), even adding this awkward construction to the end of the code generates a similar warning:

map = (Map<String,String>)hmap.getClass().cast(o);

The warning this time is Unchecked cast from capture#11-of ? extends HashMap to Map<String,String>. Trying to write:

map = HashMap<String,String>.class.cast(o);

Generates a compiler error because it can't figure out that HashMap<String,String>.class is a static class reference in the same way that e.g. HashMap.class, so we have to use a reference of the "correct" type to call Class.cast.

Is this something that Java just can't do?

Christopher Schultz
  • 20,221
  • 9
  • 60
  • 77

2 Answers2

1

Is this something that Java just can't do?

Yes, it's how it is by design.

Look at java sources (1.8) of HashMap clone method:

@SuppressWarnings("unchecked")
@Override
public Object clone() {
    HashMap<K,V> result;
    try {
        result = (HashMap<K,V>)super.clone();
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
    result.reinitialize();
    result.putMapEntries(this, false);
    return result;
}

It uses @SuppressWarnings("unchecked") for the same purposes to suppress warning on super.clone() .

You can not completely avoid it but if your code has a lot of clone() methods you can minimize these warnings by extracting to method like :

@SuppressWarnings("unchecked")
HashMap<String,String> cloneWithoutWarning(HashMap<String,String> map) { return (HashMap<String,String>) map.clone(); } 
Mike Adamenko
  • 2,944
  • 1
  • 15
  • 28
  • 1
    This is a good example. Presumably, if it were possible to avoid suppressing the warning, the code for `HashMap.clone` would use that technique. Not exactly proof-positive, but definitely illustrative. In my case, I used a local reference and suppressed the warning on only that reference (rather than the whole method). – Christopher Schultz Nov 07 '19 at 18:02
1

Usually, checking the type of a variable using instanceof and then immediately casting it will not generate such a warning.

This is not true. instanceofs have no effect on casting warnings.

This is not a warning about possible ClassCastException from this cast. Unchecked cast means Java cannot do the cast safely. That means cast might pass without ClassCastException yet the type is not a match. This might result in ClassCastException from unexpected place.

This is a real issue in this case. HashMap.clone() returns Object for backwards compatibility. There is no way to tell, if its implementation is type safe. For example:

import java.util.*;
class Main {
    public static void main(String[] args) {
        HashMap<String,String> map = new HashMap() {
            @Override
            public Object clone() {
                HashMap o = (HashMap)super.clone();
                o.put("x", new Object());
                return o;
            }
        };
        Map<String,String> copy = (Map<String,String>)map.clone(); // will pass
        System.out.println(copy.get("x")); // no cast but fails with ClassCastException
    }
}

clone() is problematic. If possible, just create a new HashMap.

If you have to use clone only use safe casts:

Map<?, ?> copy = (Map<?, ?>)map.clone();
System.out.println((String)copy.get("x")); // explicit cast will fail

Unless you are dealing with legacy code (non-generic) unchecked casts should be considered hacks/workarounds.

Piotr Praszmo
  • 17,928
  • 1
  • 57
  • 65
  • But even calling `Class.cast(obj)` still generates a warning, even though it's got all the right generic magic in it. There are some cases where these warnings *must* be suppressed in order to get them to go away. You can't code around them. – Christopher Schultz Nov 07 '19 at 17:59
  • `Class.cast` has the same problem as regular cast. You can cast to HashMap,?> but the type of elements is gone at runtime and cannot be checked. – Piotr Praszmo Nov 07 '19 at 19:29
  • Aha, the problem is with the use of specific generic types. Casting to `Map,?>` compiles without any warning *even without an instanceof check*. Using the anonymous generic type (`>`) forces the programmer to acknowledge that type-erasure exists and accept responsibility for its effects. – Christopher Schultz Nov 08 '19 at 12:54