【问题标题】:Simplify this generic method to concatenate Java arrays简化此通用方法以连接 Java 数组
【发布时间】:2012-02-28 04:54:32
【问题描述】:

我的目标是实现一个方法,它将任意数量的数组连接成一个具有它们共同超类型的数组,并返回结果(类型化)数组。我有两个实现。

第一个(这个不需要简化):

public static <T> T[] concatArrays(Class<T> type, T[]... arrays) {
    int totalLen = 0;
    for (T[] arr: arrays) {
        totalLen += arr.length;
    }
    T[] all = (T[]) Array.newInstance(type, totalLen);
    int copied = 0;
    for (T[] arr: arrays) {
        System.arraycopy(arr, 0, all, copied, arr.length);
        copied += arr.length;
    }
    return all;
}

让我们创建一些数组:

Long[] l = { 1L, 2L, 3L };
Integer[] i = { 4, 5, 6 };
Double[] d = { 7., 8., 9. };

我们的方法被调用:

Number[] n = concatArrays(Number.class, l, i, d);

这可行并且是完全类型安全的(例如,concatArrays(Long.class, l, i, d) 是编译器错误),但如果没有必要指定 Number.class 有点烦人。所以我实现了以下方法(这是我要简化的方法):

public static <T> T[] arrayConcat(T[] arr0, T[]... rest) {
    Class commonSuperclass = arr0.getClass().getComponentType();
    int totalLen = arr0.length;
    for (T[] arr: rest) {
        totalLen += arr.length;
        Class compClass = arr.getClass().getComponentType();
        while (! commonSuperclass.isAssignableFrom(compClass)) {
            if (compClass.isAssignableFrom(commonSuperclass)) {
                commonSuperclass = compClass;
                break;
            }
            commonSuperclass = commonSuperclass.getSuperclass();
            compClass = compClass.getSuperclass();
        }
    }
    T[] all = (T[]) Array.newInstance(commonSuperclass, totalLen);
    int copied = arr0.length;
    System.arraycopy(arr0, 0, all, 0, copied);
    for (T[] arr: rest) {
        System.arraycopy(arr, 0, all, copied, arr.length);
        copied += arr.length;
    }
    return all;
}

从客户的角度来看,这更好用:

Number[] n = arrayConcat(l, i, d);

同样,编译器足够聪明,可以在Long[] all = arrayConcat(l, i, d) 上给出适当的错误。由于编译器能够识别此错误,很明显我正在运行时执行编译器能够在编译时执行的工作(确定给定数组的公共超类)。有没有什么方法可以在不使用基于反射的方法来确定数组创建步骤的通用超类的情况下实现我的方法?

我试过这种方法:

public static <T> T[] arrayConcat(T[]... arrays) {
    int totalLen = 0;
    for (T[] arr: arrays) {
        totalLen += arrays.length;
    }
    Object[] all = new Object[totalLen];
    int copied = 0;
    for (T[] arr: arrays) {
        System.arraycopy(arr, 0, all, copied, arr.length);
        copied += arr.length;
    }
    return (T[]) all;
}

但这会在返回时引发 ClassCastException。显然new T[totalLen] 也出局了。有没有人有其他想法?

【问题讨论】:

  • 我认为您不应该在这里进行强制转换,我可以理解将相同类型的数组放入一个大数组中,但不要混合多种类型。
  • 考虑到这一点,我可以实现一个更简单的解决方案(例如,使用Arrays.copyOf(arr0, totalLen) 创建初始数组),但这会导致一个新问题:现在Number[] n = arrayConcat(new Long[] { 1L, 2L, 3L }, new Integer[] { 4, 5, 6 }, new Double[] { 7., 8., 9. }); 不是 编译时错误,但在运行时会导致 ArrayStoreException。
  • 你试过 ClassName.arrayConcat(l, i, d)。我认为您的 ClassCastException 来自于 T 被推断为 Long 而不是 Number 的事实。
  • @Amir 我假设您指的是this answer?正如我在上面的评论中提到的那样,这实际上失败了:Number[] n = arrayConcat(new Long[] { 1L, 2L, 3L }, new Integer[] { 4, 5, 6 }, new Double[] { 7., 8., 9. }); 不是编译时错误,但它会在运行时导致 ArrayStoreException。

标签: java arrays generics


【解决方案1】:

你可以这样做:

public static <T> T[] arrayConcat(T[]... arrays) {
    int totalLen = 0;
    for (T[] arr: arrays) {
        totalLen += arr.length;
    }
    T[] all = (T[])Array.newInstance(
        arrays.getClass().getComponentType().getComponentType(), totalLen);
    int copied = 0;
    for (T[] arr: arrays) {
        System.arraycopy(arr, 0, all, copied, arr.length);
        copied += arr.length;
    }
    return all;
}

这利用了这样一个事实,即在使用可变参数时,编译器会构造一个组件数组,并且该数组的类型已正确设置,使得组件类型为可变参数元素类型。在这种情况下,数组的类型为T[][],因此我们可以提取T 并使用它来构造我们的T[]

(一个例外是,如果调用者使用泛型类型作为可变参数类型调用它,那么编译器将无法构造正确的数组类型。但是,如果调用者这样做,它将在调用者代码中生成警告(臭名昭著的可变参数泛型警告),所以调用者被警告说坏事会发生,所以这不是我们的错。)

这个解决方案的一个惊人之处在于,即使用户传递了零个数组,它也不会产生错误的答案! (只要编译器成功编译它,它就会推断(或明确指定)一些具体类型 T 使得 T[] 是有效的返回类型。该类型 Tarrays) 显然在这种情况下编译器不会正确推断。

请注意,调用者可以手动传递“arrays”参数,在这种情况下,它的运行时类型可能为U[][],其中UT 的子类型。在这种情况下,我们的方法将返回一个运行时类型为U[] 的数组,这仍然是正确的结果,因为U[]T[] 的子类型。

【讨论】:

  • 这是一个绝妙的答案!非常聪明,正是我所希望的。谢谢!
  • @BrandonMintern @newacct,我想你会发现它不适用于零参数。看看这个,例如:pastebin.com/nV7pxxas。它在第 8 行失败,因为编译器推断出 Object[] 的可变参数数组,然后它尝试将其强制转换为 String[](导致 ClassCastException)。 javac 还警告那里未检查的泛型数组。
  • @yshavit:是的,我担心它不会推断它为零参数。然后它属于警告情况。调用者必须为零参数显式指定类型才能正确调用
  • @newacct:我是不是疯了,或者“totalLen += arrays.length”应该是“totalLen += arr.length”?
【解决方案2】:

简单的回答:不。尽管类型检查器在推断T 方面做了一些工作,但这些信息在它到达字节码时就被删除了。擦除是 Java 泛型的核心;理解它,你就会理解泛型。

顺便说一句,泛型和数组不能很好地混合。如果您尝试将一堆 List&lt;String&gt;[]s 连接成一个 List&lt;String&gt;[],您将收到编译器警告(并且通常缺乏类型安全性)。

您的示例 #2 实际上在运行时完成了更多工作,而不是编译器能够完成的工作。考虑:

Number[] longs1 = new Long[] { 1L, 2L, 3L };
Number[] longs2 = new Long[] { 4L, 5L, 6L };
Number[] concatted = arrayConcat(longs1, longs2);

编译器只知道concattedNumber[],但您的方法(在运行时)会发现公共类型实际上是Long[]

【讨论】:

    猜你喜欢
    • 2023-03-08
    • 1970-01-01
    • 2018-10-08
    • 2021-12-19
    • 1970-01-01
    • 2011-07-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多