Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to compute Map from stream, to then check property of map values?

My requirement: I have an interface that shall only contain entries such as public final static short SOME_CONST = whatever. The catch: the short constants need to be unique. And in when there are duplicates, I am mainly interested in having the SOME_CONST_A, SOME_CONST_B, ... names causing the conflict.

I wrote the below test to test that via reflection. It works, but I find it clunky and not very elegant:

@Test
public void testIdsAreUnique() {
    Map<Short, List<String>> fieldNamesById = new LinkedHashMap<>();
    Arrays.stream(InterfaceWithIds.class.getDeclaredFields())
            .filter(f -> f.getClass().equals(Short.class))
            .forEach((f) -> {
        Short key = null;
        String name = null;
        try {
            key = f.getShort(null);
            name = f.getName();
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        fieldNamesById.computeIfAbsent(key, x -> new ArrayList<>()).add(name);
    });

    assertThat(fieldNamesById.entrySet().stream().filter(e -> e.getValue().size() > 1)
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)), is(Collections.emptyMap()));
}

Is there a way to avoid that intermediate local map instance?

( bonus question: is there a nicer way to shorten the lambda that fills the map with key/value pairs? )

like image 634
GhostCat Avatar asked Sep 19 '18 12:09

GhostCat


2 Answers

Here's a stream that's grouping fields by the static value. Note some comments about other changes/corrections

Map<Short, List<String>> fieldNamesById = 
        Arrays.stream(InterfaceWithIds.class.getDeclaredFields())

         //using short.class, not Short.class
        .filter(f -> f.getType().equals(short.class)) 

        //group by value, mapping fields to their names in a list
        .collect(Collectors.groupingBy(f -> getValue(f),
                Collectors.mapping(Field::getName, Collectors.toList())));

The method called to read the value is below (primarily meant to avoid try/catch blocks in the stream):

private static Short getValue(Field f) {
    try {
        return f.getShort(null);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
like image 182
ernest_k Avatar answered Nov 06 '22 14:11

ernest_k


If you want this check efficiently (normally not so much a concern for unit test), you can reduce the work by optimistically assuming that the fields have no duplicates and performing a cheap pre-test first. Further, you can use the result of this pre-test for getting the actual field with duplicates (if there are some) without a Map.

As pre-requisite, we should encapsulate the reflective operation

private static int fieldValue(Field f) {
    try {
        return f.getShort(null);
    }
    catch(ReflectiveOperationException ex) {
        throw new IllegalStateException();
    }
}

Further, we need to map the potential values of the short value range to a positive index for a BitSet:

private static int shortToIndex(int shortValue) {
    return Math.abs(shortValue<<1) | (shortValue>>>31);
}

This assumes that numbers with smaller magnitude are more common and keeps their magnitude small, to reduce the size of the resulting BitSet. If the values are assumed to be positive, shortValue & 0xffff would be preferable. If neither applies, you could also use shortValue - Short.MIN_VALUE instead.

Having the mapping function, we can use

@Test
public void testIdsAreUnique() {
    BitSet value = new BitSet(), duplicate = new BitSet();

    Field[] fields = InterfaceWithIds.class.getDeclaredFields();
    Arrays.stream(fields)
        .filter(f -> f.getType() == short.class)
        .mapToInt(f -> shortToIndex(fieldValue(f)))
        .forEach(ix -> (value.get(ix)? duplicate: value).set(ix));

    if(duplicate.isEmpty()) return; // no duplicates

    throw new AssertionError(Arrays.stream(fields)
        .filter(f -> duplicate.get(shortToIndex(fieldValue(f))))
        .map(f -> f.getName()+"="+fieldValue(f))
        .collect(Collectors.joining(", ", "fields with duplicate values: ", "")));
}

It first fills a bitset for all encountered values and another bitset for those encountered more than once. If the latter bitset is empty, we can return immediately as there are no duplicates. Otherwise, we can use that bitset as a cheap filter to get the field having the problematic values.

like image 4
Holger Avatar answered Nov 06 '22 12:11

Holger