Reputation: 22476
I need a Collector that's nearly identical to Collectors.toSet()
, but with a custom finisher. I'd love to be able to do something like:
myCollector = Collectors.toSet();
myCollector.setFinisher(myCustomFinisher);
and be done, but that doesn't seem possible. The only alternative I can see is it essentially recreate Collectors.toSet()
using Collector.of()
, which is not very DRY.
Is there a way to take an existing Collector and amend it as described above?
EDIT
A few of the answers have recommended something like this:
Collector<T, ?, Set<T>> toSet = Collectors.toSet();
return Collector.of(
toSet.supplier(),
toSet.accumulator(),
toSet.combiner(),
yourFinisher,
toSet.characteristics());
However my custom finisher isn't actually returning a Set; it's using the accumulated set to return something else. The fact that it's doing that is putting me into Generics hell which I'm still rummaging through..
Upvotes: 6
Views: 330
Reputation: 121048
This is the implementation for a toSet
basically:
Collector.of(
HashSet::new,
Set::add,
(left, right) -> {
left.addAll(right);
return left;
});
All you have to do is add UNORDERED
Characteristics and your finisher. Looks fairly trivial to be honest. OR look at Holger's answer.
Upvotes: 4
Reputation: 234
Taking the title of your question literally, you cannot achieve what you want by "extending" a Collector
that returns a Set
, because the third generic type parameter of Collector
specifies "the result type of the reduction operation", so a Collector<?,?,R>
can never be a subclass of Collector<?,?,Set>
, unless R implements Set
.
I don't know what you want to do with the Set
as a finishing function, neither do I know the type of the elements in your Set
, so let's assume, for the sake of simplicity, that you are streaming over String
s and you want to check the number of unique String
s in the stream by returning the size of the resulting Set<String>
.
The type of your Collector
would have to be Collector<String, ?, Integer>
, so you won't get around this declaration (the fact that you want to determine the number of unique String
s via a Set<String>
is irrelevant, hence the second parameter can be ?
).
Now, naturally, you want to take advantage of Collectors.toSet()
. But how? A Collector
has 4 functions, a supplier, an accumulator, a combiner, and a finisher. From these 4, it seems like you could use the first 3 of the Collector
returned by Collectors.toSet()
. Now here's the rub: The return type of Collectors.toSet()
is actually Collector<T, ?, Set<T>>
, with the second type parameter, ?
, which denotes the accumulation type, being the problem. To explain this in non-generic terms: You have no way of knowing how the Collector
accumulates the elements internally. You only know that the finishing function will return a Set
. However, this does not necessarily mean that the items will be accumulated in a Set
. For all you know, the Collector
could accumulate the items in a List
and only at the finishing stage create a Set
from the List
's content. On the other hand, the finishing function of your custom Collector
, which calculates the size of a Set
, expects a Set
as an input value, but the Collector
returned by Collectors.toSet()
cannot guarantee that it accumulates the elements into a Set
.
Unfortunately, this means that, if you want to create a custom Collector
that accumulates the stream elements into a Set
and returns the size of this Set
at the finishing stage (or does whatever else you want to do with the Set
), the colletor returned by Collectors.toSet()
is effectively useless, since all three of the supplier
, accumulator
and combiner
pararmeters of Collector.of(Supplier, BiConsumer, BinaryOperator, Function, Collector.Characteristics)
depend on the accumulation type, which is not known of Collectors.toSet()
.
So the most viable solution seems to me to create a helper method that first collects the stream elements into a Set
using Collectors.toSet()
, and then performs the finishing transformation manually. Reading Holger's answer, it seems like a method that does this already exists.
Upvotes: 3
Reputation: 298539
That’s exactly what collectingAndThen(collector,finisher)
does.
The custom finisher does not replace the old one, if there is one, but gets combined with it (like oldFinisher.andThen(newFinisher)
), but the current implementation of toSet()
has no finisher (an identity finisher) anyway.
So all you have to do, is collectingAndThen(toSet(),myCustomFinisher)
…
Upvotes: 12