【问题标题】:Generic method replaces parametric type on the fly causing an exception通用方法动态替换参数类型导致异常
【发布时间】:2016-08-02 19:45:49
【问题描述】:

我有以下统一的方法将JSONArrays 转换成Java 数组。我承认它看起来并不像应有的那么好,但 AFAICT 这是大多数 Java 泛型可以承担这项任务的能力。

@SuppressWarnings("unchecked")
public static <T> T[] getArray(JSONArray array, Class<T> clazz)
{
  if(array == null) return null;
  int nfiles = array.length();
  if(nfiles == 0) return null;
  T result[] = (T[]) Array.newInstance(clazz, nfiles);
  for(int j = 0; j < nfiles; j++)
  {
    try
    {
      T vv = (T)array.get(j);      // temp variable for investigation, type T
                                   // assigment passes without an exception!
      if(vv.getClass() != clazz)   // outputs "java.lang.Integer!=java.lang.Long"
      {
        LOG(vv.getClass().getName() + "!=" + clazz.getName());
      }
                                   // Next line throws ArrayStoreException
      result[j] = vv;              // Integer cannot be stored in an array of type Long[]
    }
    catch(JSONException e)
    {
      e.printStackTrace();
      result[j] = null;
    }
  }
  return result;
}

可以这样使用(这只是一个例子,在真正的代码中输入数组来自分层的 JSON 结构):

Integer a[] = getArray(new JSONArray("[15,16]"), Integer.class);

在我偶然发现一个应该是Long 类型的数组之前,它似乎工作了一段时间。该方法失败,ArrayStoreException: java.lang.Integer cannot be stored in an array of type java.lang.Long[] 在线,在代码中做了相应的标记。

嗯,我知道JSONArray.get() 方法返回Object,它包含JSON 库认为适合当前输入的任何类型。如果它看到像“[15,16]”这样的数组,它会将类型推断为Integer。问题是带有 temp 变量的行应该在参数化后读取:

Long vv = (Long)array.get(j);

get()返回Integer而言,这是应该抛出类型转换异常或将Integer实例转换为Long实例的行,如果它是安全的,我认为。取而代之的是,临时变量的类型动态更改为Integer,这由日志输出确认,然后代码才会在尝试将此Integer 存储到准备好的Longs 数组时中断。 /p>

其实我可以通过这样的代码轻松解决这个问题:

if(clazz == Long.class)
{
  vv = array.getLong(j);
}

但这正是我写这个方法的原因,希望泛型可以为我处理这个问题:它的目的是消除对不同类型的许多条件检查和对特定方法的调用,例如getBooleangetString

谁能解释一下这里发生了什么以及如何修复代码以使其尽可能简单和统一?

顺便说一句,我没有找到一种方法来编写一个长数字作为 JSONObjectJSONArray 被识别为 Long 的输入文字。例如,输入“[15L,16L]”经过JSONArray解析后生成Strings ["15L", "16L"]的Java数组。我错过了什么吗?

【问题讨论】:

    标签: java android json generics type-conversion


    【解决方案1】:

    问题的核心是这一行

    T vv = (T) array.get(j); 
    

    这一行没有使用clazz 参数,并且包含一个未经检查的强制转换,它会产生一个编译警告。 regular cast 是一个虚拟机指令,它接受一个 Java 类作为输入,并根据它检查一个 Java 对象。当您使用类型变量(在这种情况下为T)时,JVM 指令实际上是对Object 的无用转换,因为在编译时没有更多信息可用。编译器会发出一个警告,指出运行时类型实际上可能不是T,但您忽略了它,并且它在运行时以ArrayStoreException 搞砸了(请注意,如果您使用List&lt;E&gt;,这甚至不会发生,因为类型参数在运行时不表示)。

    要解决您的问题,您绝对需要一系列 if 语句 - 这不是糟糕的实现,只是必须管理的环境复杂性。

    【讨论】:

    • 谢谢。只是总结(或改写)。看起来 Java 泛型允许相同的参数化类型 T 在不同的代码块行上看似解析为不同的类型,其中假定 T 是一致的,这是因为它实际上是 Object。对吗?
    • 不确定。泛型是一种具有已知范围和限制的语言特性。您的用例超出了这些限制,Java 编译器不会阻止您(因为程序可能是正确的),但会发出警告,因为它无法证明其类型安全。您忽略警告并收到运行时错误
    • 让我们忘记用例和警告,考虑一个简单的事实:有一个泛型类型 T 的方法在编译时已知为 Long(根据方法调用行)。这个 T 如何在方法内不是 Long ?变量T vv 得到Integer 类型。根据您的回答,我认为 T 实际上是一个 Object (这让我认为泛型不像我习惯的 C++ 模板那么强大和严格)。但是您对此“不确定”。所以我不明白。
    • 我不确定的事实是您对 Java 中的泛型的理解 :) 它主要是一个编译时特性。编译器无法证明 array.get(i) 返回一个 Long,实际上你必须强制转换。现在,Java 强制转换转换为 JVM 指令,只是构造 (T) 使用了在运行时不存在的类型变量,并且在字节码中被其绑定的 (Object) 替换。这个强制转换是未经检查的:你得到一个编译器警告,忽略它,然后得到一个运行时错误。 TL;DR:是的,泛型似乎没有你想象的那么强大(我不知道 C++ 模板)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-10
    • 1970-01-01
    相关资源
    最近更新 更多