在Java中如何连接两个数组?

1577

我需要在Java中合并两个String数组。

void f(String[] first, String[] second) {
    String[] both = ???
}

最简单的方法是什么?


4
Guava 中的 Bytes.concat 函数 - Ben Page
2
我看到这里有很多回复,但问题用词不当(“最简单的方法”?)无法表明最佳答案... - Artur Opalinski
3
这里有数十个答案将数据复制到一个新数组中,因为这是要求的 - 但是当不是严格必要时复制数据是一件坏事,特别是在Java中。相反,跟踪索引并将这两个数组视为已连接即可。我添加了一个说明这种技术的解决方案。 - Douglas Held
47
这个问题目前有50个不同的答案,让我想知道为什么Java从未提供一个简单的array1 + array2连接函数。 - JollyJoker
2
你可以用两行标准的Java代码完美高效地实现它(请参见我的答案),因此使用单个方法并不能获得太多好处。所有这些奇怪而神奇的解决方案都有点浪费时间。 - rghome
显示剩余2条评论
66个回答

1279

我从老牌Apache Commons Lang库中找到了一行解决方案。


ArrayUtils.addAll(T[], T...)

代码如下:

String[] both = ArrayUtils.addAll(first, second);

206
如果它回答了问题,那它为什么被称为“作弊”?确实,在这种特定情况下,具有额外的依赖可能有些过头,但指出它的存在并没有造成任何伤害,特别是考虑到Apache Commons中有许多优秀的功能。 - Rob
40
我同意,这并没有真正回答问题。高级库可以很棒,但如果你想学习一种高效的方法,你需要查看库方法所使用的代码。而且,在许多情况下,你不能仅仅在产品中即时添加另一个库。 - AdamC
88
我认为这是一个好答案。POJO方案也已经提供,但如果OP已经在他们的程序中使用Apache Commons(考虑到它的流行性,这是完全可能的),他可能仍然不知道这个解决方案。那么他就不会“为了这个方法添加一个依赖项”,而是更好地利用现有的库。 - Adam
19
如果你总是担心为单个方法添加库,那么新的库就永远不会被添加。鉴于Apache Commons中存在着许多优秀的工具,我强烈建议在第一个使用案例出现时就将其添加进来。 - Hindol
11
使用Apache Commons绝不应被称为“作弊”,我质疑那些认为它是一个不必要的依赖项的开发人员的理智。 - Jeryl Cook
显示剩余11条评论

790

以下是一个简单的方法,可以将两个数组连接起来并返回结果:

public <T> T[] concatenate(T[] a, T[] b) {
    int aLen = a.length;
    int bLen = b.length;

    @SuppressWarnings("unchecked")
    T[] c = (T[]) Array.newInstance(a.getClass().getComponentType(), aLen + bLen);
    System.arraycopy(a, 0, c, 0, aLen);
    System.arraycopy(b, 0, c, aLen, bLen);

    return c;
}

请注意,它不能处理原始数据类型,只能处理对象类型。
以下稍微复杂一些的版本适用于对象和原始数组。它使用T而不是T[]作为参数类型来实现这一点。
它还可以通过选择最通用的类型作为结果的组件类型来连接两种不同类型的数组。
public static <T> T concatenate(T a, T b) {
    if (!a.getClass().isArray() || !b.getClass().isArray()) {
        throw new IllegalArgumentException();
    }

    Class<?> resCompType;
    Class<?> aCompType = a.getClass().getComponentType();
    Class<?> bCompType = b.getClass().getComponentType();

    if (aCompType.isAssignableFrom(bCompType)) {
        resCompType = aCompType;
    } else if (bCompType.isAssignableFrom(aCompType)) {
        resCompType = bCompType;
    } else {
        throw new IllegalArgumentException();
    }

    int aLen = Array.getLength(a);
    int bLen = Array.getLength(b);

    @SuppressWarnings("unchecked")
    T result = (T) Array.newInstance(resCompType, aLen + bLen);
    System.arraycopy(a, 0, result, 0, aLen);
    System.arraycopy(b, 0, result, aLen, bLen);        

    return result;
}

以下是一个例子:
Assert.assertArrayEquals(new int[] { 1, 2, 3 }, concatenate(new int[] { 1, 2 }, new int[] { 3 }));
Assert.assertArrayEquals(new Number[] { 1, 2, 3f }, concatenate(new Integer[] { 1, 2 }, new Number[] { 3f }));

1
我喜欢这个建议,因为它不太依赖于最新的Java版本。在我的项目中,我经常被迫使用旧版本的Java或CLDC配置文件,其中一些设施(如Antti提到的那些)不可用。 - kvn
4
以下代码会破坏通用部分:concatenate(new String[]{"1"},new Object[] { new Object()})。 - dragon66
最好不要使用@SuppressWarnings注解——我将在下面发布解决方案。 - beaudet
2
哈,叫我一个纯粹主义者吧,但我更喜欢干净的代码,不需要抑制警告就能消除警告。 - beaudet
1
语义警告和“我像在C中一样使用原始内存,所以关闭你的安全性,因为我知道我在做什么”的区别在于,“我正在不安全地操作”并不是语义警告抑制。“我现在正在有意降低级别,所以停止抱怨。”感到幸运,因为Swift 5已经移除了抑制器。与Swift不同,Java仍然是为那些不害怕自己编程的程序员而设计的。 - Stephen J
显示剩余2条评论

586

在Java 8中使用Stream

String[] both = Stream.concat(Arrays.stream(a), Arrays.stream(b))
                      .toArray(String[]::new);

或者像这样,使用flatMap

String[] both = Stream.of(a, b).flatMap(Stream::of)
                      .toArray(String[]::new);

为了对泛型类型执行此操作,您需要使用反射:
@SuppressWarnings("unchecked")
T[] both = Stream.concat(Arrays.stream(a), Arrays.stream(b)).toArray(
    size -> (T[]) Array.newInstance(a.getClass().getComponentType(), size));

45
这有多有效率? - Ky -
12
值得一读:https://jaxenter.com/java-performance-tutorial-how-fast-are-the-java-8-streams-118830.html简而言之,流的性能取决于您对其进行的操作以及问题的限制。就像经常听到的那样,“取决于情况”。 - Trevor Brown
6
此外,如果 ab 是基本类型的数组,在使用 Stream.concat 作为参数时它们的流需要使用.boxed()方法转换成 Stream 类型,而不是使用 IntStream 等类型。 - Will Hardwick-Smith
30
不需要,只需要选择正确的流类型即可。例如,如果 abint[] 类型,可以使用以下代码:int[] both = IntStream.concat(Arrays.stream(a), Arrays.stream(b)).toArray(); - Holger
7
@Supuhstar: 它可能不像System.arrayCopy那样快,但也不是特别慢。你可能需要在非常性能敏感的上下文中使用巨大数组进行很多次操作才会对执行时间产生影响。 - Lii
显示剩余6条评论

499

可以编写一个完全通用的版本,甚至可以扩展以连接任意数量的数组。这些版本需要Java 6,因为它们使用Arrays.copyOf()

两个版本都避免创建任何中间List对象,并使用System.arraycopy()来确保复制大型数组的速度尽可能快。

对于两个数组,代码如下:

public static <T> T[] concat(T[] first, T[] second) {
  T[] result = Arrays.copyOf(first, first.length + second.length);
  System.arraycopy(second, 0, result, first.length, second.length);
  return result;
}

对于任意数量的数组(≥1),它看起来像这样:

public static <T> T[] concatAll(T[] first, T[]... rest) {
  int totalLength = first.length;
  for (T[] array : rest) {
    totalLength += array.length;
  }
  T[] result = Arrays.copyOf(first, totalLength);
  int offset = first.length;
  for (T[] array : rest) {
    System.arraycopy(array, 0, result, offset, array.length);
    offset += array.length;
  }
  return result;
}

11
对于原始类型的数组,您需要对每种类型进行重载:只需复制代码并将每个T替换为byte(并删除<T>)。 - Joachim Sauer
请问如何在我的类中使用<T>运算符类型? - Johnydep
6
为了进行防御性编程,我建议将以下内容添加到代码的开头:如果第一个变量(first)为空, 那么如果第二个变量(second)也为空, 则返回空值(null); 否则返回第二个变量(second)。如果第二个变量(second)为空, 则返回第一个变量(first)。 - marathon
5
@djBo: 这段代码的作用是创建一个ByteBuffer缓冲区,将array1和array2数组的内容依次写入该缓冲区,并返回合并后的字节数组。 - Sam Goldberg
25
如果您使用不同组件类型的数组调用这些函数,例如concat(ai, ad),其中aiInteger[],而adDouble[],那么这种方法中会出现一个错误(在这种情况下,编译器将类型参数<T>解析为<? extends Number>)。通过Arrays.copyOf创建的数组将具有第一个数组的组件类型,例如在这个例子中是Integer。当函数要复制第二个数组时,将抛出一个ArrayStoreException。解决办法是增加一个额外的Class<T> type参数。 - T-Bull
@BrettOkken:是的 - 你说得对。我不知道为什么会这样建议...(可能是因为我刚刚写了将字节数组连接起来的代码...) - Sam Goldberg

206

或者使用备受喜爱的Guava

String[] both = ObjectArrays.concat(first, second, String.class);

此外,还有适用于原始数组的版本:

  • Booleans.concat(first, second)
  • Bytes.concat(first, second)
  • Chars.concat(first, second)
  • Doubles.concat(first, second)
  • Shorts.concat(first, second)
  • Ints.concat(first, second)
  • Longs.concat(first, second)
  • Floats.concat(first, second)

尽管我很喜欢Guava,但Apache Commons的方法在处理可空值方面更好。 - Ravi Wallau
7
虽然使用库是好的,但不幸的是问题已经被抽象化了。因此,根本的解决方案仍然难以捉摸。 - KRK Owner
54
抽象化有什么问题?不知道这里重新发明轮子的问题在哪里,如果想了解问题,请查看源代码或阅读相关内容。专业代码应使用高级别库,最好是在Google内部开发! - Breno Salgado
1
@SébastienTromp 这是这个问题的最佳解决方案 - ArrayUtils。 - Ravi Wallau
3
我不建议仅为了连接数组而将Guava添加到项目中,因为它太大且缺乏严重的模块化。@BrenoSalgado "如果是在Google内部开发的话会好得多" 我强烈反对这个想法。 - gouessej
显示剩余3条评论

176

你可以用两行代码将这两个数组连接起来。

String[] both = Arrays.copyOf(first, first.length + second.length);
System.arraycopy(second, 0, both, first.length, second.length);

这是一个快速高效的解决方案,适用于原始数据类型,因为这两种方法都被重载了。

您应该避免使用涉及ArrayList、streams等的解决方案,因为它们需要为没有任何用处的临时内存分配空间。

对于大数组,您应该避免使用for循环,因为它们效率并不高。内置的方法使用块复制函数,非常快速。


17
这是最佳解决方案之一。100%标准Java。快速/高效。应该获得更多赞! - Shebla Tsama
它可能很高效,但与您预期的相比有点混乱:both = array1 + array2 - Carson Holzheimer

59

使用Java API:

String[] f(String[] first, String[] second) {
    List<String> both = new ArrayList<String>(first.length + second.length);
    Collections.addAll(both, first);
    Collections.addAll(both, second);
    return both.toArray(new String[both.size()]);
}

17
这段话的意思是:虽然使用ArrayList生成一个数组并在toArray方法中生成另一个数组的效率不高,但由于易读性好,因此仍然是有效的。我的翻译如下:这种方法虽然效率不高,因为需要先用ArrayList创建一个数组,再在toArray方法中生成另一个数组,但是由于简单易懂,仍然有效。 - PhoneixS
1
适用于字符串和对象(如问题所述),但是对于基本类型(如整数),没有addAll方法。 - JRr
1
正如这篇文章所阐述的那样,使用both.toArray(new String[0])both.toArray(new String[both.size()])更快,即使它与我们的直觉相矛盾。这就是为什么在优化时测量实际性能如此重要的原因。或者当无法证明更复杂的变体的优势时,只需使用更简单的结构即可。 - Holger

43
一个100%使用旧版Java,并且不包含System.arraycopy(例如在GWT客户端中不可用)的解决方案:
static String[] concat(String[]... arrays) {
    int length = 0;
    for (String[] array : arrays) {
        length += array.length;
    }
    String[] result = new String[length];
    int pos = 0;
    for (String[] array : arrays) {
        for (String element : array) {
            result[pos] = element;
            pos++;
        }
    }
    return result;
}

重新修改了我的File[],但是还是一样的。感谢您的解决方案。 - ShadowFlame
5
可能相当低效,不过。 - Jonas Czech
你可能想要添加null检查。并且或许将一些变量设置为final - Tripp Kinetics
@TrippKinetics 的 null 检查会隐藏 NPE 而不是显示它们,而在本地变量中使用 final 没有任何好处(至少目前还没有)。 - Maarten Bodewes
个人而言,我会使用带有索引的普通 for 循环,而不是 for-each。这极不可能被重构为使用列表(你只需编写另一个方法),而且通过索引直接访问数组很可能比 for-each 更慢。挑剔一点:pos 应该在第一个 for 循环内部定义,而不是在外部定义。 - Maarten Bodewes
2
@Maarten Bodewes 我认为你会发现(如果你进行基准测试,我已经做过了),在Java的后期版本中,for-each循环与索引循环运行时间相同。优化器会处理它。 - rghome

34

最近我遇到了过多的内存旋转问题。如果a和/或b通常为空,这里是silvertab代码的另一种适应方式(也进行了泛型化):

private static <T> T[] concatOrReturnSame(T[] a, T[] b) {
    final int alen = a.length;
    final int blen = b.length;
    if (alen == 0) {
        return b;
    }
    if (blen == 0) {
        return a;
    }
    final T[] result = (T[]) java.lang.reflect.Array.
            newInstance(a.getClass().getComponentType(), alen + blen);
    System.arraycopy(a, 0, result, 0, alen);
    System.arraycopy(b, 0, result, alen, blen);
    return result;
}

编辑:此前的版本中提到,像这样重复使用数组应该有明确的文档记录。正如Maarten在评论中指出的那样,通常最好只是删除if语句,从而避免需要文档记录。但是再说一遍,这些if语句是这种优化的整个重点。我将保留此答案,但要小心!


5
这意味着你返回相同的数组,更改返回数组中的值会改变输入数组相同位置的值。 - Lorenzo Boccaccia
是的 - 请参见我在帖子末尾关于数组重用的评论。在我们特定的情况下,这种解决方案所施加的维护开销是值得的,但在大多数情况下应该使用防御性复制。 - volley
Lorenzo / volley,你能解释一下代码中哪个部分导致数组被重用吗?我以为System.arraycopy只是复制数组的内容呢? - Rosdi Kasim
4
通常,调用者期望调用concat()方法返回一个新分配的数组。但是,如果a或b为空,concat()将返回其中一个传递给它的数组。这种重复使用可能会令人意外。(是的,arraycopy只执行复制操作。重复使用来自于直接返回a或b中的一个。) - volley
代码应该尽可能自我解释。阅读代码的人不应该查找被调用函数的JavaDoc,以找出它在一个特定条件下执行一件事情,在另一个条件下执行另一件事情。简而言之:通常不能通过注释来解决这些设计问题。最简单的解决方法是直接省略这两个“if”语句。 - Maarten Bodewes
显示剩余2条评论

27

Functional Java库提供了一个数组包装类,它为数组提供了方便的方法,如连接。

import static fj.data.Array.array;

...然后

Array<String> both = array(first).append(array(second));

要获取未包装的数组,请调用:
String[] s = both.array();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接