Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Add prefix and suffix to Collectors.joining() only if there are multiple items present

I have a stream of strings:

Stream<String> stream = ...;

I want to construct a string which concatenates these items with , as a separator. I do this as following:

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

Now I want add a prefix [ and a suffix ] to this output only if there were multiple items. For example:

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

Can this be done without first materializing the Stream<String> to a List<String> and then checking on List.size() == 1? In code:

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(",")) + "]";
}

It feels odd to first convert the stream to a list and then again to a stream to be able to apply the Collectors.joining(","). I think it's suboptimal to loop through the whole stream (which is done during a Collectors.toList()) only to discover if there is one or more item(s) present.

I could implement my own Collector<String, String> which counts the number of given items and use that count afterwards. But I am wondering if there is a directer way.

This question intentionally ignores there case when the stream is empty.

like image 922
John Doe Avatar asked Oct 05 '18 19:10

John Doe


People also ask

What does collectors joining () do?

joining. Returns a Collector that concatenates the input elements, separated by the specified delimiter, with the specified prefix and suffix, in encounter order.

How do you add a prefix and suffix to a string in Java?

replace(w, "PREFIX_"+ w +"_SUFFIX"); } } System. out. println(str); Note here that I am using java Set to parse unique words from the input string and then replacing them in the original string with the added prefix/suffix.

What is a collector in Java 8?

Collectors is a final class that extends Object class. It provides reduction operations, such as accumulating elements into collections, summarizing elements according to various criteria, etc. Java Collectors class provides various methods to deal with elements. Methods.


2 Answers

There is already an accepted answer and I upvoted it too.

Still I would like to offer potentially another solution. Potentially because it has one requirement:
The stream.spliterator() of Stream<String> stream needs to be Spliterator.SIZED.

If that applies to your case, you could use also this solution:

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

According to the JavaDoc Spliterator.getExactSizeIfKnown() "returns estimateSize() if this Spliterator is SIZED, else -1." If a Spliterator is SIZED then "estimateSize() prior to traversal or splitting represents a finite size that, in the absence of structural source modification, represents an exact count of the number of elements that would be encountered by a complete traversal."

Since "most Spliterators for Collections, that cover all elements of a Collection report this characteristic" (API Note in JavaDoc of SIZED) this could be the desired directer way.

EDIT:
If the Stream is empty we can return an empty String at once. If the Stream has only one String there is no need to create a StringJoiner and to copy the String to it. We return the single String directly.

  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();
  }
like image 190
LuCio Avatar answered Nov 15 '22 16:11

LuCio


Yes, this is possible using a custom Collector instance that will use an anonymous object with a count of items in the stream and an overloaded toString() method:

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

Explanation

Collector interface has the following methods:

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

I will omit the last method as it is not relevant for this example.

There is a collect() method with the following signature:

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

and in our case it would resolve to:

<Object> Object collect(Supplier<Object> supplier,
              BiConsumer<Object, ? super String> accumulator,
              BiConsumer<Object, Object> combiner);
  • In the supplier, we are using an instance of StringJoiner (basically the same thing that Collectors.joining() is using).
  • In the accumulator, we are using StringJoiner::add() but we increment the count as well
  • In the combiner, we are using StringJoiner::merge() and add the count to the accumulator
  • Before returning from format() function, we need to call toString() method to wrap our accumulated StringJoiner instance in [] (or leave it as is is, in case of a single-element stream

The case for an empty case could also be added, I left it out in order not to make this collector more complicated.

like image 32
syntagma Avatar answered Nov 15 '22 17:11

syntagma