Roy Truelove
Roy Truelove

Reputation: 22476

Extend an existing stream collector instance

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

Answers (3)

Eugene
Eugene

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

Stingy
Stingy

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 Strings and you want to check the number of unique Strings 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 Strings 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

Holger
Holger

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

Related Questions