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

wnvonmuf  于 2023-01-29  发布在  Java
关注(0)|答案(3)|浏览(171)

我有一串弦:

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>来计算给定项的数量,然后使用这个计数,但是我想知道是否有更直接的方法。
这个问题故意忽略了流为空的情况。

np8igboo

np8igboo1#

是的,这可以通过使用一个定制的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示例 Package 在[]中(或者在单元素流的情况下,保持原样

一个空箱子的箱子也可以加进去,我把它留了出来,为了不让这个收集器变得更复杂。

euoag5mw

euoag5mw2#

已经有一个公认的答案,我也投了赞成票。
我仍然想提供另一种可能的解决方案。可能是因为它有一个要求:
Stream<String> 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();
  }

根据JavaDoc,Spliterator.getExactSizeIfKnown() "如果SpliteratorSIZED,则返回estimateSize(),否则返回-1"。如果SpliteratorSIZED,则"在遍历或拆分之前的estimateSize()表示有限大小,在没有结构源修改的情况下,表示完整遍历将遇到的元素数目的精确计数。
由于"大多数用于集合的拆分器,覆盖Collection的所有元素,报告此特征"(SIZED的JavaDoc中的API注解),这可能是所需的 * 直接方式 *。

    • 编辑:**

如果Stream为空,我们可以立即返回一个空的String;如果Stream只有一个String,则不需要创建一个StringJoiner,也不需要将String复制到StringJoiner,我们直接返回单个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();
  }
cngwdvgl

cngwdvgl3#

我知道我迟到了,但我最近也遇到了同样的问题,我想也许值得记录一下我是如何解决这个问题的。
虽然公认的解决方案确实有效,但我认为它比实际需要的要复杂得多,使其难以阅读和理解。与定义一个Object实现,然后分离访问和修改所述对象状态的累加器和组合器函数相反,为什么不定义自己的收集器呢?在上面提出的问题中,这可以通过以下方式实现:

Collector<CharSequence, StringJoiner, String> collector = new Collector<>() {

    int count = 0;

    @Override
    public Supplier<StringJoiner> supplier() {
        return () -> new StringJoiner(",");
    }

    @Override
    public BiConsumer<StringJoiner, CharSequence> accumulator() {
        return (joiner, sequence) -> {
            count++;
            joiner.add(sequence);
        };
    }

    @Override
    public BinaryOperator<StringJoiner> combiner() {
        return StringJoiner::merge;
    }

    @Override
    public Function<StringJoiner, String> finisher() {
        return (joiner) -> {
            String joined = joiner.toString();
            return (count > 1) ? "[" + joined + "]" : joined;
        };
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }
};

原理是一样的,你有一个计数器,每当元素被添加到StringJoiner时,它就会递增。基于添加的元素总数,finisher决定StringJoiner的结果是否应该用方括号括起来。这是不言而喻的,但是你甚至可以定义一个特定于这个实现的单独的类。允许它在整个代码库中使用。您甚至可以更进一步,使它更通用以支持更广泛的输入类型,添加自定义前缀/后缀参数,等等。说到使用,只需将收集器的示例传递给Streamcollect方法:

return list.stream().collect(collector);

相关问题