Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I combine the results from a Collectors.groupingBy

I'm playing with java reflection and learning more about Stream.collect.

I have an annotation MyTag that has two properties (id and type enum[Normal|Failure]). Also, I have a list of annotated methods with MyTag and I was able to group those methods by the id property of the MyTag annotation using Collectors.groupingBy:

List<Method> ml = getMethodsAnnotatedWith(anClass.getClass(),
                                           MyTag.class);
Map<String, List<Method>> map = ml.stream().collect(groupingBy(m -> {
      var ann = m.getDeclaredAnnotation(MyTag.class);
      return ann.anId();
    }, TreeMap::new, toList()));

Now I need to reduce the resulting List to one single object composed of ONLY TWO items of the same MyTag.id, one with a MyTag.type=Normal and the other with a MyTag.type=Failure. So it would result in something like a Map<String, Pair<Method, Method>>. If there are more than two occurrences, I must just pick the first ones, log and ignore the rest.

How could I achieve that ?

like image 404
Cristiano Avatar asked Dec 31 '22 16:12

Cristiano


2 Answers

You can use

Map<String, Map<Type, Method>> map = Arrays.stream(anClass.getClass().getMethods())
    .filter(m -> m.isAnnotationPresent(MyTag.class))
    .collect(groupingBy(m -> m.getDeclaredAnnotation(MyTag.class).anId(),
            TreeMap::new,
            toMap(m -> m.getDeclaredAnnotation(MyTag.class).aType(),
                  m -> m, (first, last) -> first,
                  () -> new EnumMap<>(Type.class))));

The result maps the annotations ID property to a Map from Type (the enum constants NORMAL and FAILURE) to the first encountered method with a matching annotation. Though “first” has not an actual meaning when iterating over the methods discovered by Reflection, as it doesn’t guaranty any specific order.

The () -> new EnumMap<>(Type.class) map factory is not necessary, it would also work with the general purpose map used by default when you don’t specify a factory. But the EnumMap will handle your case of having only two constants to map in a slightly more efficient way and its iteration order will match the declaration order of the enum constants.

I think, the EnumMap is better than a Pair<Method, Method> that requires to remember which method is associated with “normal” and which with “failure”. It’s also easier to adapt to more than two constants. Also, the EnumMap is built-in and doesn’t require a 3rd party library.

like image 78
Holger Avatar answered Jan 02 '23 07:01

Holger


The following example can easily be adapted to your code:

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.stream.Collectors;

import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Test {

    private static final Logger logger = LoggerFactory.getLogger(Test.class);

    public static void main(String[] args) {
        List<Pair<String, String>> ml = Arrays.asList(
                Pair.of("key1", "value1"),
                Pair.of("key1", "value1"),
                Pair.of("key1", "value2"),
                Pair.of("key2", "value1"),
                Pair.of("key2", "value3"));

        Map<String, Pair<String, String>> map = ml.stream().collect(
                Collectors.groupingBy(m -> {
                    return m.getKey();
                }, TreeMap::new, Collectors.toList()))
                .entrySet()
                .stream()
                .collect(Collectors.toMap(
                        Map.Entry::getKey, e -> convert(e.getValue())));

        System.out.println(map.values());
    }

    private static Pair<String, String> convert(List<Pair<String, String>> original) {
        long count1 = original.stream().filter(e -> Objects.equals(e.getValue(), "value1")).count();
        long count2 = original.stream().filter(e -> Objects.equals(e.getValue(), "value2")).count();
        if (count1 > 1) {
            logger.warn("More than one occurrence of value1");
        }
        if (count2 > 1) {
            logger.warn("More than one occurrence of value2");
        }
        return Pair.of(count1 > 0 ? "value1" : null,
                count2 > 0 ? "value2" : null);
    }

}
  • Instead of Pair<String, String> use Method
  • m.getDeclaredAnnotation(MyTag.class).anId() corresponds to pair.getKey()

The folowing result is printed to the console:

01:23:27.959 [main] WARN syglass.Test2 - More than one occurrence of value1
[(value1,value2), (value1,null)]
like image 30
Mykhailo Skliar Avatar answered Jan 02 '23 07:01

Mykhailo Skliar