javacollectionsguavajava-streamjoiner

Is it possible to apply joiner (collector, accumulator) over function using Java Guava?


Is it possible to collect Strings over collection while grouping? That's how it works in Java 8:

Map<String, String> discountOptions = p.getDiscountOptions().Stream()
    .collect(groupingBy(
        this::getDiscountName,
        Collectors.mapping(this::getValue, Collectors.joining(","))));

I'm curious, is there concise way to do it in Google Guava? That's how I try to replicate it in Guava:

Map<String, Collection<String>> stringCollectionMap = Multimaps.transformValues(
    Multimaps.index(p.getDiscountOptions(), 
        new Function<DiscountOption, String>() {
            @Override
            public String apply(DiscountOption d) {
                return getDiscountName(d);
            }
        }),
    new Function<DiscountOption, String>() {
        @Override
        public String apply(DiscountOption d) {
            return getValue(d);
        }
    }).asMap();

Map<String, String> discountOptions =  Maps.transformValues(
    stringCollectionMap,
    new Function<Collection<String>, String>() {
        @Override
        public String apply(Collection<String> strings) {
            return Joiner.on(",").join(strings);
        }
    });

Solution

  • You're not going to get anything more concise than the Java 8 streams API, since the reason it exists is to improve these kind of operations.

    Pre-Java 8 functional programming can be jury-rigged with Guava's functional utilities, but as they warn:

    As of Java 7, functional programming in Java can only be approximated through awkward and verbose use of anonymous classes.... Excessive use of Guava's functional programming idioms can lead to verbose, confusing, unreadable, and inefficient code.... Imperative code should be your default, your first choice as of Java 7.

    Here's an imperative translation of your code:

    private static final Joiner COMMA_JOINER = Joiner.on(",");
    
    ListMultimap<String, String> groupedByDiscountName = ArrayListMultimap.create();
    for (DiscountOption option : p.getDiscountOptions()) {
      groupedByDiscountName.put(getDiscountName(option), getValue(option));
    }
    
    ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
    for (Entry<String, Collection<String>> e : groupedByDiscountName.asMap().entrySet()) {
      builder.put(e.getKey(), COMMA_JOINER.join(e.getValues());
    }
    Map<String, String> discountOptions = builder.build();
    

    It's shorter and easier to read. Given your current API this is roughly the best you can do with Java 7.

    That said you might consider re-examining your API - in particular it's odd that you'd use static methods (getDiscountName(), getValue()) to extract data from your DiscountOption class - these seem like clear candidates to be instance methods. Similarly you might consider creating a Discounts class that contains one or more DiscountOption instances and provides a toString() that returns your comma separated string. Then you just need to construct your Discounts objects and you're good to go.