【问题标题】:Java generics - any way to avoid casts (and unchecked warnings) after I have called instanceof?Java 泛型 - 在我调用 instanceof 之后,有什么方法可以避免强制转换(和未经检查的警告)?
【发布时间】:2013-04-30 16:21:56
【问题描述】:

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) 并得到我想要的东西

【问题讨论】:

  • 看不到没有反射的解决方案,用于根据 T 的类型调用 getXXX() 方法。那会是一团糟。
  • 顺便说一句,你不需要同时转换为 T 和布尔值 -- (T) (Boolean) 可以变成 (T)。几乎任何时候你像这样连续施放两件东西,你都可以施放最后一个。请记住,强制转换只是告诉编译器如何解释数据,这是不变的。你写的内容是“取这个布尔值,将其解释为布尔值,然后将其解释为 T”。你可以说“取这个布尔值并将其解释为 T”
  • @yshavit:错了——我不能
  • @Mr_and_Mrs_D 我一定记错了关于自动装箱的确切规格,抱歉。
  • @yshavit: 你不能投射一个基元

标签: java android generics


【解决方案1】:

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

更长的答案:如您所知,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 正试图保护您免受这种情况的影响。

下面是一个例子。在此示例中,我将使用 Number 代替 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 函数几乎相同
  • 我故意创建了一个错误:如果 defaultValLong,我会得到(并转换为 T)一个 double 而不是 long。但是类型系统无法捕捉到这个错误,这正是未经检查的演员试图警告我的(它警告我它无法捕捉任何错误,而不是一定有错误)。
  • 如果defaultValue 是一个int 或String,一切都会好起来的。但如果它是 Long,并且 num 为空,那么当调用站点需要 Long 时,我将返回 Double
  • 因为我的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 的结果转换为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

【讨论】:

  • 我将查看您更新的答案(我刚刚看到但看看这个:bild.me/bild.php?file=4385173GENERICS.jpg。所以不:“它会变成它可以转换为最具体的类型是,在这种情况下是 Object"。这个 receive() 只是 &lt;T&gt; T retrieve(String key, T value) { return AccessPreferences.retrieve(this, key, value); } 其中 AccesPrefs.receive 是有问题的函数
  • @Mr_and_Mrs_D 不是 100% 确定您的评论是什么意思,抱歉。你是说IDE说它会在方法中添加一个演员吗?如果是这样......它不会。在您的屏幕截图中,它将在呼叫站点添加演员表。因此,haveEnabledWifi 将变为 return (Boolean) retrieve(HAVE_ENABLED_WIFI_PREF_KEY, false),这正是您想要的。但在上面,我展示了一个示例,其中调用站点不会 插入强制转换(因为调用站点本身是通用的),导致您能够破坏Map。基本上,javac 很难证明对T 的强制转换是安全的,所以它表现得比较保守。
  • 赶时间 - 不是 IDE - 是 javac 这么说的,ofc。 Eclipse 不实现自己的编译器。是 javac 将其视为返回布尔值 - 而不是 Eclipse。它会像这样编译。在您的回答中,您忘记在prefsToMap 中添加地图的使用。另外 - 如果您使用 getObject 的场景是真的,我敢打赌您会在 Integer val = (Integer) map.get("foo"); 中将 Unsafe 从 Object 转换为 Integer。也许试试看? (并认为这真的不是我在泛型和我的班级中最大的黑暗点:)
  • Eclipse 确实有自己的编译器,或者至少习惯了!但这不是问题所在,而是 JLS 本身。需要明确的是,Integer val = (Integer)... 行不是你要写的任何东西,当你有 Map&lt;String,Integer&gt; map = whatever(), Integer val = map.get("foo"); 时,javac/ecj 将插入它我想你和我可能会在这里讨论交叉路径。 :)
  • 交叉路径?那是什么 ?我的英语让我失望了——无论如何我知道eclipse有一个增量编译器,但它在图片中显示的是javac将编译成的代码。考虑:如果whatever()Map&lt;String, Object&gt; 类型,如果您调用prefsToMap("foo", new Object(), map);(编辑prefsToMap),您将在Integer val = map.get("foo"); 行中收到警告正是因为编译器即将添加演员表我>。如果您的地图不是Map&lt;String, Object&gt;,您将在prefsToMap("foo", new Object(), map); 行中收到警告。请使用虚拟 getObject 进行测试
【解决方案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;
    }

}

【讨论】:

  • "类型不匹配:无法从 capture#4-of 转换?将对象扩展为 T"
  • 查看我的编辑——如果我可以从defval某种方式(而不是传递它)获得clazz,这可能会起作用——注意defval 可能是原始的。反正我能做到吗? (顺便说一句,纠正错别字并避免空行 - 它只会让人们滚动 - 让你的 getXXX 一个衬垫 - 一旦有滚动条,你就为这样的网站发布了太多代码:)
  • 只有一个'unhecked'警告,并保证defaultValue不为空,你可以: Class clazz = (Class)defaultValue.getClass();
【解决方案3】:

好吧,我的问题 is 是(简单地):既然我已经使用了 instanceof 并且 T 已经归结为一个特定的类,有没有办法避免单一和双重强制转换以及警告:未选中?

答案是no - 我引用了这个特定的答案,因为它表明我不是唯一一个想知道的人。但是您可能希望通过@yshavit 投票给有趣的尽管有点离题answer :)

【讨论】:

    猜你喜欢
    • 2012-03-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多