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.
joining. Returns a Collector that concatenates the input elements, separated by the specified delimiter, with the specified prefix and suffix, in encounter order.
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.
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.
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();
}
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();
}
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);
supplier
, we are using an instance of StringJoiner
(basically the same thing that Collectors.joining()
is using). accumulator
, we are using StringJoiner::add()
but we increment the count as wellcombiner
, we are using StringJoiner::merge()
and add the count to the accumulatorformat()
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 streamThe case for an empty case could also be added, I left it out in order not to make this collector more complicated.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With