将 List<char[]> 转换为数组 char[] 而不使用 System.arraycopy()

Ror*_*ory 14 java arrays char java-stream

在 JavaList<char[]>中将a 转换/扁平化 的简单方法是什么?char[]

我知道我可以通过迭代List和 using来做到这一点System.arraycopy,但我想知道是否有更简单的方法来使用 Java 8 流来做到这一点?

也许是这样的,但不必将原语装箱charCharacter

List<char[]> listOfCharArrays = ...

Character[] charArray =
    Stream.of(listOfCharArrays )
        .flatMap(List::stream)
        .toArray(Character[]::new);
Run Code Online (Sandbox Code Playgroud)

Mic*_*ael 18

这是我能想到的最易读的版本。您可以通过 StringBuilder 将所有 char 数组附加到 String,然后将其转换为char[].

char[] chars = listOfCharArrays.stream()
    .collect(Collector.of(StringBuilder::new, StringBuilder::append, StringBuilder::append, StringBuilder::toString))
    .toCharArray();
Run Code Online (Sandbox Code Playgroud)

可能比迭代版本慢得多,因为arrayCopy可以复制内存块。

您可以考虑预先计算字符总数以避免 StringBuilder 数组重新分配,但这种优化和任何其他优化都会消耗您从使用流中获得的可读性收益。

int totalSize = listOfCharArrays.stream().mapToInt(arr -> arr.length).sum();
char[] chars = listOfCharArrays.stream()
    .collect(Collector.of(() -> new StringBuilder(totalSize), //... the same
Run Code Online (Sandbox Code Playgroud)

有 2 个不必要的副本(StringBuilder-> StringString-> char[]),这实际上是这些类不完全适合此任务的结果。CharBuffer更适合;请参阅马丁的回答


Maa*_*wes 16

我只能想到一件事,那就是使用CharBuffer. 出于效率原因,我总是首先计算正确的大小,然后执行复制。任何执行多个副本和/或执行字符串处理的解决方案都是低效的。

这是代码。第一行计算所需数组的总大小,然后为其分配足够的内存。第二行使用上述方法执行复制put。最后一行返回char[]支持 的CharBuffer

CharBuffer fullBuffer = CharBuffer.allocate(
        listOfCharArrays.stream().mapToInt(array -> array.length).sum());
listOfCharArrays.forEach(fullBuffer::put);
char[] asCharArray = fullBuffer.array();
Run Code Online (Sandbox Code Playgroud)

当然,我不能保证它不会在方法System.arrayCopy内部的某个地方使用CharBuffer#put。我强烈希望它将System.arrayCopy在内部使用或类似的代码。不过,这可能适用于此处提供的大多数解决方案。

如果可以估计最大大小,则可以通过使用足够大的缓冲区来避免第一次大小计算,但这将需要缓冲区中数据的额外副本;只是返回正确大小的后备数组,这意味着数据仅被复制一次CharBuffer#array


CharBuffer如果你想使用面向对象的代码也可以直接使用。请注意,您需要确保flip在写入后它是可变的(您可以使用或方法CharBuffer传递副本- 返回的实例引用相同的缓冲区,但具有独立的、可变的“位置”和“限制”字段)。duplicateasReadOnly

BufferJava NIO 类有点难以理解,但是一旦理解了,您就会从它们中获得巨大的好处,例如,将它们用于CharEncoder内存映射文件时。

  • 如果你不想预先确定大小,可以使用`CharArrayWriter w = new CharArrayWriter(); listOfCharArrays.forEach(a -&gt; w.write(a, 0, a.length)); char[] asCharArray = w.toCharArray();` (3认同)

Joo*_*gen 7

正如 Holger 所评论的,它可以通过 String 或更确切地说 CharBuffer 来完成。

char[] flatten(List<char[]> list) {
    return list.stream()
        .map(CharBuffer::wrap) // Better than String::new
        .collect(Collectors.joining())
        .toCharArray();
}
Run Code Online (Sandbox Code Playgroud)

这需要“完整”数组在开始或结束处没有任何不完整的代理字符对。

因此,将其与以下内容进行比较:

char[] flatten(List<char[]> list) {
    int totalLength = list.stream().mapToInt(a -> a.length).sum();
    char[] totalArray = new char[totalLength];
    int i = 0;
    for (char[] array : list) {
        System.arraycopy(array, 0, totalArray, i, array.length);
        i += array.length; 
    }
    return totalArray;
}
Run Code Online (Sandbox Code Playgroud)

差别不大,而且代码更可靠。

或者将整个软件置于不可变状态String而不是char[].


Ale*_*nko 6

这是一个使用 Stream API 的解决方案,它不需要额外的内存分配,这在您使用时不可避免地会发生StringStringBuilder因为即使使用 Java 8,也不可能String在不制作数据的中间副本的情况下实例化 a ,并且StringBuilder会给您访问它的底层数组,而不是为您提供副本,而且自 Java 9 以来,两者String都由StringBuilder数组byte[]而不是字符数组支持)。

首先,计算结果数组的大小是有意义的(正如 @Maarten Bodewes 和 @Michael 在他们的答案中已经提到的),这是一个非常快的操作,因为我们不处理这些数组的数据,而只是请求它们每个的长度。

然后,为了构造结果数组,我们可以使用收集器,它将流元素累积到底层char[]数组中,然后在处理所有流元素后将其分发出去,而无需任何中间转换并分配额外的内存。

收集器的所有功能都需要是无状态的,并且更改应该只发生在其可变容器内。因此,我们需要一个可变容器来包装char[]数组,但它不应该有像那样的强封装StringBuilder,即允许访问其底层数组。我们可以通过CharBuffer.

因此,基本上与@Maarten Bodewes 在答案中引入的想法相同,完全通过流实现。

CharBuffer.allocate(length)在幕后将实例化char[]给定长度,并将CharBuffer.array()返回相同的数组而不生成额外的副本。

public static void main(String[] args) {
    
    List<char[]> listOfCharArrays =
        List.of(new char[]{'a', 'b', 'c'},
                new char[]{'d', 'e', 'f'},
                new char[]{'g', 'h', 'i'});

    char[] charArray = listOfCharArrays.stream()
        .collect(Collectors.collectingAndThen(
            Collectors.summingInt(arr -> arr.length),  // calculating the total length of the arrays in the list
            length -> listOfCharArrays.stream().collect(
                Collector.of(
                    () -> CharBuffer.allocate(length), // mutable container of the collector
                    CharBuffer::put,                   // accumulating stream elements inside the container
                    CharBuffer::put,                   // merging the two containers with partial results (runs only when stream is being executed in parallel)
                    CharBuffer::array                  // finisher function performs the final transformation
                ))
        ));

    System.out.println(Arrays.toString(charArray));
}
Run Code Online (Sandbox Code Playgroud)

输出:

[a, b, c, d, e, f, g, h, i]
Run Code Online (Sandbox Code Playgroud)

  • @AlexanderIvanchenko 一般来说,如果您必须添加诸如“不能并行工作”或“假设实现细节 xyz”之类的注释,那么您根本不应该使用 Stream API。事实上,无法提供正确的合并功能意味着两者兼而有之,假设实现细节(“只有并行流使用合并功能”)并需要顺序执行。 (3认同)
  • 是的,将其与并行流一起使用会增加内存消耗并增加可避免的复制开销,但这并不是一个独特的情况。在很多情况下,将 Stream 转为并行会损害性能。但至少,结果应该保持正确。• 事实上,基于“StringBuilder”的方法具有额外的复制开销,有时甚至超过预期。从 Java 9 开始,内部数组对每个字符使用一个字节,直到遇到第一个非拉丁字符。然后,必须对整个内容物进行充气。 (2认同)