1

I was wondering how to count different fields of an object using a single stream. I know I can easily count a single property of an object using streams (countedWithStream) or even using a for to count several at once (countedWithFor). But I would actually love to know if it would be possible to achieve the same as countedWithFor but using a single stream, generating the same output.

import com.google.common.collect.ImmutableMap;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.LongStream;

import static java.util.stream.Collectors.*;

class Scratch {
public static void main(String[] args) {

    List<AnObject> objects = createObjects();

    Map<String, Map<Long, Long>> countedWithStream = countUsingStream(objects);
    Map<String, Map<Long, Long>> countedWithFor = countUsingFor(objects);
}

private static Map<String, Map<Long, Long>> countUsingStream(List<AnObject> objects) {
    BiFunction<List<AnObject>, Function<AnObject, Long>, Map<Long, Long>> count = (ojs, mpr) -> ojs.stream()
                                                                                                   .collect(groupingBy(mpr, counting()));

    return ImmutableMap.<String, Map<Long, Long>>builder().put("firstId", count.apply(objects, AnObject::getFirstId))
                                                          .put("secondId", count.apply(objects, AnObject::getSecondId))
                                                          .build();
}
private static Map<String, Map<Long, Long>> countUsingFor(List<AnObject> objects) {
    Map<Long, Long> firstIdMap = new HashMap<>();
    Map<Long, Long> secondIdMap = new HashMap<>();

    final BiFunction<Long, Map<Long, Long>, Long> count = (k, m) -> k != null ? m.containsKey(k) ? m.put(k, m.get(k) + 1L) : m.put(k, 1L) : null;

    for (AnObject object : objects) {
        count.apply(object.firstId, firstIdMap);
        count.apply(object.secondId, secondIdMap);
    }

    return ImmutableMap.<String, Map<Long, Long>>builder().put("firstId", firstIdMap)
                                                          .put("secondId", secondIdMap)
                                                          .build();
}

private static List<AnObject> createObjects() {
    return LongStream.range(1, 11)
                     .mapToObj(Scratch::createObject)
                     .collect(toList());
}

private static AnObject createObject(long id) {
    return new AnObject(id, id);
}

private static class AnObject {
    public final long firstId;
    public final long secondId;

    public AnObject(long firstId, 
                    long secondId) {
        this.firstId = firstId;
        this.secondId = secondId;
    }

    public long getFirstId() {
        return firstId;
    }

    public long getSecondId() {
        return secondId;
    }
}
2
  • Not really clear what is incorrect with the first approach(countUsingStream) that you've shared. What is it that you're looking further for? Commented Feb 1, 2019 at 9:53
  • There isn't anything incorrect using the first approach, except it uses 2n iterations and the second approach n. I would love to use the concise syntax / functional style of streams achieving n iterations. Commented Feb 1, 2019 at 9:56

2 Answers 2

1

You can use a reduce to do the job in n iterations with something like this:

Supplier<Map<String, Map<Long, Long>>> mapSupplier = () -> {
    Map<String, Map<Long, Long>> outputMap = new HashMap<>();
    outputMap.put("firstId", new HashMap<>());
    outputMap.put("secondId", new HashMap<>());
    return outputMap;
};

Map<String, Map<Long, Long>> reduce = objects.stream().collect(mapSupplier,
        (acc, obj) -> {
            acc.get("firstId").merge(obj.firstId, 1L, (curv, incr) -> curv + incr);
            acc.get("secondId").merge(obj.secondId, 1L, (curv, incr) -> curv + incr);
        }
        , (acc1, acc2) -> {
            acc2.get("firstId").forEach((k, v) -> acc1.get("firstId").merge(k, v, (v1, v2) -> v1 + v2));
            acc2.get("secondId").forEach((k, v) -> acc1.get("secondId").merge(k, v, (v1, v2) -> v1 + v2));
        });

But this may not be as concise as you want it to be.

Sign up to request clarification or add additional context in comments.

Comments

1

You could implement the custom collector, see example in this article:

public class Scratch {

    public static final String FIRST_ID = "firstId";

    public static final String SECOND_ID = "secondId";

    private static class AnObjectFieldCounter implements Collector<AnObject, Map<String, Map<Long, Long>>, Map<String, Map<Long, Long>>> {
        @Override
        public Supplier<Map<String, Map<Long, Long>>> supplier() {
            return HashMap::new;
        }

        @Override
        public BiConsumer<Map<String, Map<Long, Long>>, AnObject> accumulator() {
            return (map, obj) -> {
                Map<Long, Long> inner;

                inner = map.getOrDefault(FIRST_ID, new HashMap<>());
                inner.compute(obj.getFirstId(), (id, count) -> (count == null) ? 1 : count + 1);
                map.put(FIRST_ID, inner);

                inner = map.getOrDefault(SECOND_ID, new HashMap<>());
                inner.compute(obj.getSecondId(), (id, count) -> (count == null) ? 1 : count + 1);
                map.put(SECOND_ID, inner);
            };
        }

        @Override
        public BinaryOperator<Map<String, Map<Long, Long>>> combiner() {
            return (a, b) -> {
                Map<Long, Long> firstIdCountMap = Stream
                        .concat(a.get(FIRST_ID).entrySet().stream(), b.get(FIRST_ID).entrySet().stream())
                        .collect(groupingBy(Map.Entry::getKey, Collectors.summingLong(Map.Entry::getValue)));

                Map<Long, Long> secondIdCountMap = Stream
                        .concat(a.get(SECOND_ID).entrySet().stream(), b.get(SECOND_ID).entrySet().stream())
                        .collect(groupingBy(Map.Entry::getKey, Collectors.summingLong(Map.Entry::getValue)));

                Map<String, Map<Long, Long>> result = new HashMap<>();
                result.put(FIRST_ID, firstIdCountMap);
                result.put(SECOND_ID, secondIdCountMap);
                return result;
            };
        }


        @Override
        public Function<Map<String, Map<Long, Long>>, Map<String, Map<Long, Long>>> finisher() {
            return Function.identity();
        }

        @Override
        public Set<Characteristics> characteristics() {
            return new HashSet<>(Arrays.asList(UNORDERED, IDENTITY_FINISH));
        }
    }

    public static void main(String[] args) {

        List<AnObject> objects = createObjects();

        Map<String, Map<Long, Long>> countedWithCollector = countUsingCollector(objects);
        Map<String, Map<Long, Long>> countedWithStream = countUsingStream(objects);
        Map<String, Map<Long, Long>> countedWithFor = countUsingFor(objects);
    }

    private static Map<String, Map<Long, Long>> countUsingCollector(List<AnObject> objects) {
        Map<String, Map<Long, Long>> result = objects.stream().collect(new AnObjectFieldCounter());
        return ImmutableMap.<String, Map<Long, Long>>builder().putAll(result).build();
    }

    //...
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.