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

2022-09-04 22:29:37

我有一个字符串流:

Stream<String> stream = ...;

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

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

现在,仅当有多个项目时,我才希望向此输出添加前缀和后缀。例如:[]

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

这可以在不首先实现为a然后检查的情况下完成吗?在代码中: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(",")) + "]";
}

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

我可以实现我自己的计数给定项目的数量,然后使用该计数。但我想知道是否有导演的方式。Collector<String, String>

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


答案 1

是的,使用自定义实例可以做到这一点,该实例将使用具有流中项目计数的匿名对象和重载方法:CollectortoString()

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();
}

解释

收集器接口具有以下方法:

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);
  • 在 中,我们使用的是 的一个实例(基本上与正在使用的东西相同)。supplierStringJoinerCollectors.joining()
  • 在 中,我们正在使用,但我们也增加了计数accumulatorStringJoiner::add()
  • 在 中,我们正在使用并将计数添加到累加器combinerStringJoiner::merge()
  • 在从函数返回之前,我们需要调用方法来包装我们的累积实例(或者在单元素流的情况下保持原样)format()toString()StringJoiner[]

也可以添加空盒的情况,我将其省略了,以免使该收集器更加复杂。


答案 2

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

我仍然想提供另一种潜在的解决方案。可能是因为它有一个要求:
的需要是 .stream.spliterator()Stream<String> streamSpliterator.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 的说法,“如果这是 ,则返回 。如果 a 是 SIZE,则 “在遍历或拆分之前表示有限大小,在没有结构源修改的情况下,表示完全遍历将遇到的元素数的精确计数。Spliterator.getExactSizeIfKnown()estimateSize()SpliteratorSIZED-1SpliteratorestimateSize()

由于“大多数用于集合的拆分器,它涵盖了报告此特征的所有元素”(JavaDoc中的API注释)这可能是所需的控制器方式CollectionSIZED

编辑:
如果 为空,我们可以立即返回空。如果 只有一个,则无需创建并复制到它。我们直接返回单曲。StreamStringStreamStringStringJoinerStringString

  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();
  }