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