1

Android 代码 - SharedPreferences 类导出不同的方法来保存/检索不同的首选项:

@SuppressWarnings("unchecked")
public static <T> T retrieve(Context ctx, String key, T defaultValue) {
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
    if (defaultValue instanceof Boolean) return (T) (Boolean) prefs
            .getBoolean(key, (Boolean) defaultValue);
    else if (defaultValue instanceof Float) return (T) (Float) prefs
            .getFloat(key, (Float) defaultValue);
    // etc - another 4 cases
}

这行得通,我可以打电话boolean stored = retrieve(ctx, "BOOLEAN_KEY", true)- 但我的问题是:既然我已经使用instanceofT归结为一个特定的类,有没有办法避免单一和双重演员表和warning : unchecked

编辑:如果我要通过类,我不妨打电话getBoolean()getFloat()等等。我想要的是简化方法的内部并摆脱警告,但仍然能够调用retrieve(ctx, "KEY", 1 or "string" or true)并得到我想要

4

3 回答 3

4

简短的回答:不,您无法摆脱警告。他们在那里是有原因的。

更长的答案:您可能知道,Java 中的泛型只是语法糖加上编译时检查;几乎没有任何东西可以保存到运行时(一个称为“擦除”的过程)。这意味着(T)您的方法中的强制转换实际上是无操作的。它将转换为最具体的类型,在本例中为Object. 所以这:

(T) (Boolean) prefs.whatever()

真的变成了这样:

(Object) (Boolean) prefs.whatever()

这当然和刚才一样:

(Boolean) prefs.whatever()

这会让你陷入危险的境地,这正是警告试图告诉你的。基本上,您正在失去类型安全性,它最终可能会让您远离错误的实际位置(因此难以追踪)。想象一下:

// 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");

到目前为止一切都很好,在你的情况下它会起作用,因为如果“foo”在首选项中,你会打电话getInt来获取它。但是想象一下,如果您的retrieve函数中有一个错误,例如if( defaultValue instanceof Integer)意外返回getDouble()而不是getInt()(使用强制转换等)。编译器不会捕捉到它,因为你的强制转换T实际上只是一个强制转换Object,这总是被允许的!直到Integer val = map.get("foo");变成:

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

这个演员可能离错误真正发生的地方很远 -getObject呼叫 - 使得很难追踪。Javac 试图保护您免受这种情况的影响。

这是一个将所有内容放在一起的示例。在这个例子中,我将使用 aNumber而不是 prefs 对象,只是为了简单起见。您可以复制粘贴此示例并按原样试用。

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);
    }
}

需要注意的几点:

  • 最重要的是:尽管泛型应该给我类型安全,但我得到了 ClassCastException。不仅如此,我在一段完全没有错误的代码中得到了错误(main)。错误发生在prefsToMap,但main付出了代价。如果map是一个实例变量,则可能很难跟踪引入该错误的位置。
  • 除了使用数字而不是首选项之外,我的功能与您的功能getNumber几乎相同retrieve
  • 我故意制造了一个错误:如果defaultVal是 a Long,我得到(并转换为T)一个精度而不是长整数。但是类型系统无法捕获这个错误,这正是未经检查的演员试图警告我的(它警告我它无法捕获任何错误,而不是一定有错误)。
  • 如果defaultValue是 int 或 String,一切都会好起来的。但是,如果它是 Long,并且num为 null,那么Double当调用站点需要一个Long.
  • 因为我的prefsToMap类只转换为T——如上所述,它是一个无操作转换——它不会导致任何转换异常。直到倒数第二行,我才得到例外Long fromMap = map.get("foo")

使用javap -c,我们可以看到其中一些在字节码中的样子。首先,让我们看一下getNumber。请注意,演员T表不会显示为任何内容:

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

接下来,看看getLong。请注意,它将结果转换getNumber为 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

最后,这是prefsToMap. 请注意,由于它只处理泛型T类型——也就是 Object——它根本不进行任何转换。

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
于 2013-04-30T16:53:20.113 回答
2

正常的方法是使用 Class.cast(obj),但是你需要一个 T 类的实例,这通常是通过将一个传递给方法来完成的,但在你的情况下会很好:

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

编辑:在评论之后,是的,这可能是类型不匹配的问题。

您需要将类类型作为方法的一部分传递,或者从默认值推断它(如果它不为空:

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

使用工作示例再次编辑(这对我没有任何警告):

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;
    }

}
于 2013-04-30T16:28:31.940 回答
0

那么我的问题是(简单地说):因为我已经使用了 instanceof 并且 T 已经归结为一个特定的类,有没有办法避免单一和双重强制转换和警告:未检查?

答案是否定的——我引用了这个特定的答案,因为它表明我不是唯一一个想知道的人。但是您可能希望对@yshavit 的有趣但有点离题的答案进行投票:)

于 2013-07-18T23:26:56.837 回答