Java generics - any way to avoid casts (and unchecked warnings) after I have called instanceof?

后端 未结 3 1861
Happy的楠姐
Happy的楠姐 2021-01-25 07:19

Android code - the SharedPreferences class exports different methods for persisting/retrieving different preferences :

@SuppressWarnings(\"unchecked\")
public st         


        
相关标签:
3条回答
  • 2021-01-25 07:59

    Short answer: no, you can't get rid of the warnings. They're there for a reason.

    Longer answer: As you may know, generics in Java are just syntactic sugar plus compile-time checks; pretty much nothing survives to runtime (a process known as "erasure"). This means that the cast to (T) within your method is actually a no-op. It'll turn into a cast to the most specific type that it can be, which in this case is Object. So this:

    (T) (Boolean) prefs.whatever()
    

    really turns into this:

    (Object) (Boolean) prefs.whatever()
    

    which is of course the same as just:

    (Boolean) prefs.whatever()
    

    This can get you into a dangerous situation, which is what the warnings are trying to tell you. Basically, you're losing type safety, and it can end up biting you far away from where the bug actually is (and thus be hard to track down). Imagine the following:

    // wherever you see "T" here, think "Object" due to erasure
    public <T> void prefsToMap(String key, T defaultValue, Map<String, T> map) {
        T val = retrieve(this.context, key, defaultValue);
        map.put(key, val);
    }
    
    Map<String,Integer> map = new HashMap<>();
    prefsToMap("foo", 123, map);
    // ... later
    Integer val = map.get("foo");
    

    So far so good, and in your case it'll work, because if "foo" is in the prefs, you'll call getInt to get it. But imagine if you had a bug in your retrieve function, such that if( defaultValue instanceof Integer) accidentally returned getDouble() instead of getInt() (with the casting and all that). The compiler won't catch it, since your cast to T is really just a cast to Object, which is always allowed! You won't find out until Integer val = map.get("foo");, which becomes:

    Integer val = (Integer) map.get("foo"); // cast automatically inserted by the compiler
    

    This cast could be very far away from where the error really happened -- the getObject call -- making it hard to track down. Javac is trying to protect you from that.

    Here's an example of it all put together. In this example, I'll be using a Number instead of a prefs object, just to keep things simple. You can copy-paste this example and try it out as is.

    import java.util.*;
    
    public class Test {
        @SuppressWarnings("unchecked")
        public static <T> T getNumber(Number num, T defaultVal) {
            if (num == null)
                return defaultVal;
            if (defaultVal instanceof Integer)
                return (T) (Integer) num.intValue();
            if (defaultVal instanceof String)
                return (T) num.toString();
            if (defaultVal instanceof Long)
                return (T) (Double) num.doubleValue(); // oops!
            throw new AssertionError(defaultVal.getClass());
        }
    
        public static void getInt() {
            int val = getNumber(null, 1);
        }
    
        public static void getLong() {
            long val = getNumber(123, 456L); // This would cause a ClassCastException
        }
    
        public static <T> void prefsToMap(Number num, String key, T defaultValue, Map<String, T> map) {
            T val = getNumber(num, defaultValue);
            map.put(key, val);
        }
    
        public static void main(String[] args) {
            Map<String, Long> map = new HashMap<String,Long>();
            Long oneTwoThree = 123L;
            Long fourFixSix = 456L;
            prefsToMap(oneTwoThree, "foo", fourFixSix, map);
            System.out.println(map);
            Long fromMap = map.get("foo"); // Boom! ClassCastException
            System.out.println(fromMap);
        }
    }
    

    A few things to note:

    • The big one: Even though generics are supposed to give me type-safety, I got a ClassCastException. And not just that, but I got the error in a section of code with no errors at all (main). The error happened in prefsToMap, but main paid the cost. If the map were an instance variable, it could be very hard to track where that error was introduced.
    • Other than using a Number instead of a prefs, my getNumber is pretty much the same as your retrieve function
    • I intentionally created a bug: if defaultVal is a Long, I get (and cast to T) a double instead of a long. But the type system has no way of catching this bug, which is exactly what the unchecked cast is trying to warn me about (it's warning me that it can't catch any bugs, not that there necessarily are bugs).
    • If defaultValue is an int or String, everything will be fine. But if it's a Long, and the num is null, then I'll be returning a Double such when the call site expects a Long.
    • Because my prefsToMap class only casts to T -- which, as mentioned above, is a no-op cast -- it won't cause any cast exceptions. I don't get an exception until the second-to-last line, Long fromMap = map.get("foo").

    Using javap -c, we can see how some of these look in bytecode. First, let's look at getNumber. Note that the casts to T don't show up as anything:

    public static java.lang.Object getNumber(java.lang.Number, java.lang.Object);
      Code:
       0:   aload_0
       1:   ifnonnull   6
       4:   aload_1
       5:   areturn
       6:   aload_1
       7:   instanceof  #2; //class java/lang/Integer
       10:  ifeq    21
       13:  aload_0
       14:  invokevirtual   #3; //Method java/lang/Number.intValue:()I
       17:  invokestatic    #4; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       20:  areturn
       21:  aload_1
       22:  instanceof  #5; //class java/lang/String
       25:  ifeq    33
       28:  aload_0
       29:  invokevirtual   #6; //Method java/lang/Object.toString:()Ljava/lang/String;
       32:  areturn
       33:  aload_1
       34:  instanceof  #7; //class java/lang/Long
       37:  ifeq    48
       40:  aload_0
       41:  invokevirtual   #8; //Method java/lang/Number.doubleValue:()D
       44:  invokestatic    #9; //Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
       47:  areturn
       48:  new #10; //class java/lang/AssertionError
       51:  dup
       52:  aload_1
       53:  invokevirtual   #11; //Method java/lang/Object.getClass:()Ljava/lang/Class;
       56:  invokespecial   #12; //Method java/lang/AssertionError."<init>":(Ljava/lang/Object;)V
       59:  athrow
    

    Next, take a look at getLong. Notice that it casts the result of getNumber to a Long:

    public static void getLong();
      Code:
       0:   bipush  123
       2:   invokestatic    #4; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       5:   ldc2_w  #15; //long 456l
       8:   invokestatic    #17; //Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
       11:  invokestatic    #13; //Method getNumber:(Ljava/lang/Number;Ljava/lang/Object;)Ljava/lang/Object;
       14:  checkcast   #7; //class java/lang/Long
       17:  invokevirtual   #18; //Method java/lang/Long.longValue:()J
       20:  lstore_0
       21:  return
    

    And finally, here's prefsToMap. Notice that since it only deals with the generic T type -- aka Object -- it doesn't do any casting at all.

    public static void prefsToMap(java.lang.Number, java.lang.String, java.lang.Object, java.util.Map);
      Code:
       0:   aload_0
       1:   aload_2
       2:   invokestatic    #13; //Method getNumber:(Ljava/lang/Number;Ljava/lang/Object;)Ljava/lang/Object;
       5:   astore  4
       7:   aload_3
       8:   aload_1
       9:   aload   4
       11:  invokeinterface #19,  3; //InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
       16:  pop
       17:  return
    
    0 讨论(0)
  • 2021-01-25 08:00

    The normal way is to use the Class.cast(obj), biut you will need an instance of the T Class, this is typically done by passing one in to the method, but in your case will be fine:

    return Boolean.class.cast(pregs.getBoolean(key, (Boolean)defaultValue));
    

    EDIT: After the comment, yes, it could be a problem with the type mismatch.

    You will need to pass in the class type as part of your method, or infer it from the default value (if it is not null:

    return defaultValue.getClass().cast(pregs.getBoolean(key, (Boolean)defaultValue));
    

    Editing again with working example (this works wihtout any warnings for me):

    public class JunkA {
    
        private boolean getBoolean(String key, boolean def) {
            return def;
        }
    
        private float getFloat(String key, float def) {
            return def;
        }
    
        private String getString(String key, String def) {
            return def;
        }
    
        private int getInt(String key, int def) {
            return def;
        }
    
        public <T> T getProperty(final Class<T> clazz, final String key,
                final T defval) {
            if (clazz.isAssignableFrom(Boolean.class)) {
                return clazz.cast(getBoolean(key, (Boolean) defval));
            }
            if (clazz.isAssignableFrom(String.class)) {
                return clazz.cast(getString(key, (String) defval));
            }
            if (clazz.isAssignableFrom(Boolean.class)) {
                return clazz.cast(getFloat(key, (Float) defval));
            }
            if (clazz.isAssignableFrom(Integer.class)) {
                return clazz.cast(getInt(key, (Integer) defval));
            }
            return defval;
        }
    
    }
    
    0 讨论(0)
  • 2021-01-25 08:19

    Well my question is was (simply) : since I already used instanceof and T has boiled down to a specific class is there a way to avoid the single and double casts and the warning : unchecked ?

    And the answer is no - I am quoting this particular answer cause it shows I am not the only one who wondered. But you may wish to vote the interesting albeit a bit off topic answer by @yshavit :)

    0 讨论(0)
提交回复
热议问题