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

后端 未结 3 1865
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  void prefsToMap(String key, T defaultValue, Map map) {
        T val = retrieve(this.context, key, defaultValue);
        map.put(key, val);
    }
    
    Map 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 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  void prefsToMap(Number num, String key, T defaultValue, Map map) {
            T val = getNumber(num, defaultValue);
            map.put(key, val);
        }
    
        public static void main(String[] args) {
            Map map = new HashMap();
            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."":(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
    

提交回复
热议问题