Javagenerics – 在我调用instanceof后避免强制转换(和未经检查的警告)的任何方法?

Android代码 – SharedPreferences类导出用于持久化/检索不同首选项的不同方法:

@SuppressWarnings("unchecked") public static  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)好吧 – 但我的问题是:因为我已经使用了instanceof而且T已经归结为一个特定的类是否有办法避免单一和双演员和warning : unchecked

编辑 :如果我要传递类,我可能会调用getBoolean()getFloat()等。我想要的是简化方法的内部并摆脱警告,但仍然能够调用retrieve(ctx, "KEY", 1 or "string" or true)并得到我想要的东西

简答:不,你不能摆脱警告。 他们在那里是有原因的。

更长的答案:正如您所知,Java中的generics只是语法糖加上编译时检查; 几乎所有东西都没有存活到运行时(一个称为“擦除”的过程)。 这意味着你方法中的转换为(T)实际上是无操作。 它将变成一个特定类型的强制转换,在这种情况下是Object 。 所以这:

 (T) (Boolean) prefs.whatever() 

真的变成了这个:

 (Object) (Boolean) prefs.whatever() 

这当然与以下相同:

 (Boolean) prefs.whatever() 

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

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

到目前为止一切都那么好,在你的情况下它会起作用,因为如果“foo”在prefs中,你会调用getInt来获取它。 但是想象一下,如果你的retrieve函数中有一个错误, if( defaultValue instanceof Integer)意外地返回getDouble()而不是getInt() (带有转换和所有这些)。 编译器不会捕获它,因为你对T的强制转换实际上只是对Object的强制转换,这总是被允许的! 在Integer val = map.get("foo");之前你不会发现Integer val = map.get("foo"); ,成为:

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

这个演员可能离错误真正发生的地方 – getObject调用 – 很难跟踪。 Javac试图保护你免受这种伤害。

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

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

有几点需要注意:

  • 最重要的一点:尽管generics应该给我类型安全性,但我得到了一个ClassCastException。 而不仅仅是这样,但我在一段代码中得到了错误,完全没有错误( main )。 错误发生在prefsToMap ,但main支付了成本。 如果map是实例变量,则可能很难跟踪引入错误的位置。
  • 除了使用Number而不是prefs之外,我的getNumber与你的retrieve函数几乎相同
  • 我故意创建了一个错误:如果defaultValLong ,我得到(并转换为T )一个double而不是long。 但是类型系统无法捕获这个bug,这正是未经检查的演员试图警告我的事情(它警告我它不会捕获任何错误,而不是必然存在错误)。
  • 如果defaultValue是一个int或String,一切都会好的。 但是如果它是一个Long,并且num为null,那么当调用站点需要Long时,我将返回一个Double
  • 因为我的prefsToMap类只强制转换为T – 如上所述,它是一个无操作转换 – 它不会导致任何强制转换exception。 直到第二个到最后一行, Long fromMap = map.get("foo") ,我才会得到exception。

使用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."":(Ljava/lang/Object;)V 59: athrow 

接下来,看看getLong 。 请注意,它将getNumber的结果强制转换为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 。 请注意,因为它只处理genericsT类型 – 也就是对象 – 它根本不进行任何转换。

 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 

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

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

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

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

 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 getProperty(final Class 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; } } 

好吧,我的问题 是(简单): 因为我已经使用了instanceof而且T已经归结为一个特定的类有没有办法避免单一和双重演员和警告:未选中?

答案是否定的 – 我引用这个特别的答案,因为它表明我不是唯一一个想知道的人。 但你可能希望投票有趣但尽管有点偏离@yshavit 🙂