【问题标题】:Add prefix and suffix to Collectors.joining() only if there are multiple items present仅在存在多个项目时为 Collectors.joining() 添加前缀和后缀
【发布时间】:2018-10-05 19:29:52
【问题描述】:

我有一个字符串流:

Stream<String> stream = ...;

我想构造一个字符串,将这些项目与, 作为分隔符连接起来。我这样做如下:

stream.collect(Collectors.joining(","));

现在我只想在有多个项目的情况下为此输出添加前缀 [ 和后缀 ]。例如:

  • a
  • [a,b]
  • [a,b,c]

如果不首先将Stream&lt;String&gt; 实现为List&lt;String&gt;,然后检查List.size() == 1,是否可以做到这一点?在代码中:

public String format(Stream<String> stream) {
    List<String> list = stream.collect(Collectors.toList());

    if (list.size() == 1) {
        return list.get(0);
    }
    return "[" + list.stream().collect(Collectors.joining(",")) + "]";
}

首先将流转换为列表,然后再转换为流以便能够应用Collectors.joining(","),这感觉很奇怪。我认为循环遍历整个流(在Collectors.toList() 期间完成)只是为了发现是否存在一个或多个项目是次优的。

我可以实现我自己的Collector&lt;String, String&gt;,它计算给定项目的数量并在之后使用该计数。但我想知道是否有更直接的方法。

这个问题故意忽略了流为空时的情况。

【问题讨论】:

  • “构造一个字符串”还是“构造一个流”?
  • Streams 有 count() 方法给出列表中元素的数量,这样你就不必具体化流,而是使用它而不是 list.size()跨度>
  • @AmanJ count()terminal operation。之后stream 就不能再使用了,它被消耗掉了。
  • 没有比制作自己的收藏家更直接的方法了。
  • 这有点超出您的问题范围,但您可以重构代码的最后一行:stream.collect(Collectors.joining("-", "[", "]")); =))

标签: java string java-8 java-stream


【解决方案1】:

是的,这可以使用自定义 Collector 实例来实现,该实例将使用具有流中项目计数的匿名对象和重载的 toString() 方法:

public String format(Stream<String> stream) {
    return stream.collect(
            () -> new Object() {
                StringJoiner stringJoiner = new StringJoiner(",");
                int count;

                @Override
                public String toString() {
                    return count == 1 ? stringJoiner.toString() : "[" + stringJoiner + "]";
                }
            },
            (container, currentString) -> {
                container.stringJoiner.add(currentString);
                container.count++;
            },
            (accumulatingContainer, currentContainer) -> {
                accumulatingContainer.stringJoiner.merge(currentContainer.stringJoiner);
                accumulatingContainer.count += currentContainer.count;
            }
                         ).toString();
}

说明

Collector接口有以下方法:

public interface Collector<T,A,R> {
    Supplier<A> supplier();
    BiConsumer<A,T> accumulator();
    BinaryOperator<A> combiner();
    Function<A,R> finisher();
    Set<Characteristics> characteristics();
}

我将省略最后一个方法,因为它与本示例无关。

有一个带有以下签名的collect() 方法:

<R> R collect(Supplier<R> supplier,
              BiConsumer<R, ? super T> accumulator,
              BiConsumer<R, R> combiner);

在我们的例子中,它会解析为:

<Object> Object collect(Supplier<Object> supplier,
              BiConsumer<Object, ? super String> accumulator,
              BiConsumer<Object, Object> combiner);
  • supplier 中,我们使用StringJoiner 的实例(基本上与Collectors.joining() 使用的相同)。
  • accumulator 中,我们使用的是 StringJoiner::add(),但我们也会增加计数
  • combiner 中,我们使用StringJoiner::merge() 并将计数添加到累加器中
  • 在从format() 函数返回之前,我们需要调用toString() 方法将我们累积的StringJoiner 实例包装在[] 中(或者在单元素流的情况下保持原样

空箱的箱子也可以加,为了不让这个收集器更复杂,我把它省略了。

【讨论】:

  • 同样可以使用Stream.reduce(...) 来实现,其中identitynew Object() { ... }accumulator return container;,然后combinerreturn accumulatingContainer; 但比您的解决方案更长,因此您的解决方案仍然更可取。
  • @LuCio ...它会被定义破坏,因为reduce 不适用于可变容器。这正是collect 的用途。
  • @Holger:你是对的。问题中的任务需要mutable reduction。并且文档说:可变归约操作称为collect(),因为它将所需的结果收集到一个结果容器中。我的方法滥用了普通的reduction operation,因为accumulator 包装了一个容器。 - 感谢您的澄清。
【解决方案2】:

已经有一个被接受的答案,我也投了赞成票。

我仍然想提供另一种解决方案。可能是因为它有一个要求:
Stream&lt;String&gt; streamstream.spliterator() 必须是 Spliterator.SIZED

如果这适用于您的情况,您也可以使用此解决方案:

  public String format(Stream<String> stream) {
    Spliterator<String> spliterator = stream.spliterator();
    StringJoiner sj = spliterator.getExactSizeIfKnown() == 1 ?
      new StringJoiner("") :
      new StringJoiner(",", "[", "]");
    spliterator.forEachRemaining(sj::add);

    return sj.toString();
  }

根据JavaDocSpliterator.getExactSizeIfKnown()“如果这个SpliteratorSIZED,则返回estimateSize(),否则返回-1。”如果SpliteratorSIZED,则“estimateSize() 在遍历或拆分之前表示一个有限大小,在没有结构源修改的情况下,它表示完整遍历将遇到的元素数量的精确计数。”

由于“涵盖Collection 的所有元素的大多数集合拆分器都报告了此特性”(SIZED 的 JavaDoc 中的 API 注释),这可能是所需的更直接的方式。 p>

编辑:
如果Stream 为空,我们可以立即返回一个空的String。如果Stream 只有一个String,则无需创建StringJoiner 并将String 复制到其中。我们直接返回单个String

  public String format(Stream<String> stream) {
    Spliterator<String> spliterator = stream.spliterator();

    if (spliterator.getExactSizeIfKnown() == 0) {
      return "";
    }

    if (spliterator.getExactSizeIfKnown() == 1) {
      AtomicReference<String> result = new AtomicReference<String>();
      spliterator.tryAdvance(result::set);
      return result.get();
    }

    StringJoiner result = new StringJoiner(",", "[", "]");
    spliterator.forEachRemaining(result::add);
    return result.toString();
  }

【讨论】:

  • 如果您知道 Stream 有一个 String 元素,您可以直接返回该单个元素,省略复制到 StringJoiner 和新的 String。但是您还应该处理流没有已知大小但最终只有一个元素的情况。这不是一个奇怪的极端案例,即Stream.of("foo").filter(s -&gt; true) 已经属于该类别。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-07-12
  • 1970-01-01
  • 2017-12-07
  • 2022-06-22
  • 2018-11-01
  • 1970-01-01
相关资源
最近更新 更多