仅当存在多个项目时,才将前缀和后缀添加到Collectors.joining()

时间:2018-10-05 19:29:53

标签: java string java-8 java-stream

我有一串字符串:

Stream<String> stream = ...;

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

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

现在,仅当存在多个项目时,我才想在此输出中添加前缀[和后缀]。例如:

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

是否可以先将Stream<String>体现为List<String>然后再检查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<String, String>来计算给定项目的数量,然后使用该数量。但是我想知道是否有更直接的方法。

该问题有意忽略了流为空时的情况。

2 个答案:

答案 0 :(得分:4)

是的,这可以通过使用自定义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实例包装在[]中(或保留原样,以防单元素流

还可以添加一个空盒子的盒子,为了不使此收集器变得更加复杂,我将其省略。

答案 1 :(得分:0)

已经有一个可以接受的答案,我也赞成。

我仍然想提供另一种解决方案。可能是因为它有一个要求:
stream.spliterator()中的Stream<String> stream必须为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();
  }

根据JavaDoc Spliterator.getExactSizeIfKnown()“如果此estimateSize()Spliterator,则返回SIZED,否则返回-1。”如果SpliteratorSIZED,则遍历或拆分之前的“ estimateSize()”表示有限的大小,在没有结构源修改的情况下,该大小表示要计算的元素数量的精确计数被完整遍历所遇到。”

由于“大多数用于Collection的所有元素的集合拆分器均报告了此特征”(SIZED的JavaDoc中的API注释),因此这可能是理想的直接方式 >。

编辑:
如果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();
  }