Как вычислить Map из потока, чтобы потом проверить свойство значений карты? - PullRequest
0 голосов
/ 19 сентября 2018

Мое требование: у меня есть интерфейс, который должен содержать только записи, такие как public final static short SOME_CONST = whatever.Подвох: короткие константы должны быть уникальными .А когда есть дубликаты, меня больше всего интересуют имена SOME_CONST_A, SOME_CONST_B, ..., вызывающие конфликт.

Я написал тест ниже, чтобы проверить это с помощью отражения.Это работает, но я нахожу это неуклюжим и не очень элегантным:

@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()));
}

Есть ли способ избежать этого промежуточного локального экземпляра карты?

(бонусный вопрос: есть ли лучший способ сократить лямбду, заполняющую карту парами ключ / значение?)

Ответы [ 5 ]

0 голосов
/ 19 сентября 2018

Если вы хотите эту проверку эффективно (обычно это не так важно для модульного теста), вы можете сократить объем работы, если оптимистично предположить, что поля не имеют дубликатов, и сначала выполните предварительный дешевый тест.Кроме того, вы можете использовать результат этого предварительного теста для получения фактического поля с дубликатами (если они есть) без Map.

. В качестве предварительного условия мы должны инкапсулировать операцию отражения

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

Далее, нам нужно отобразить потенциальные значения диапазона значений short в положительный индекс для BitSet:

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

Это предполагает, что числа с меньшей величинойчаще встречается и сохраняет их величину небольшой, чтобы уменьшить размер получаемой BitSet.Если значения предполагаются положительными, shortValue & 0xffff будет предпочтительным.Если ни то, ни другое не применимо, вы также можете использовать shortValue - Short.MIN_VALUE.

Имея функцию отображения, мы можем использовать

@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: ", "")));
}

Сначала она заполняет набор битов для всех встречающихся значений и другой набор битов для тех, которые встречались более одного раза.Если последний набор битов пуст, мы можем немедленно вернуться, так как дубликатов нет.В противном случае мы можем использовать этот набор битов в качестве дешевого фильтра, чтобы получить поле с проблемными значениями.

0 голосов
/ 19 сентября 2018

Не уверен, что это будет соответствовать вашим потребностям, но почему бы просто:

 ...filter(..)
    .collect(Collectors.toMap(f -> f.getShort(null), Field::getName))

Если есть дубликаты, произойдет сбой с исключением.Поймай это и сделай Assert.fail(...) например.

Надеюсь, я правильно понял код, набрав его на телефоне

0 голосов
/ 19 сентября 2018

Несколько проблем здесь.Во-первых, f.getClass() даст вам класс Field экземпляра, а не фактический класс поля.Вы хотите

f.getType().equals(Short.class)

Далее, вы должны помнить, что Short.class и short.class различны, поэтому вы действительно хотите

f.getType().equals(Short.class) || f.getType().equals(short.class)

Я бы лично воспользовался тем фактом, чтоmap.put возвращает предыдущее значение для данного ключа.Поскольку мы надеемся, что предыдущее значение никогда не будет, мы можем просто вызвать assertNull для результата.

Весь ваш тест будет выглядеть так:

Map<Short, String> fieldNamesById = new LinkedHashMap<>();
Arrays.stream(InterfaceWithIds.class.getDeclaredFields())
    .filter(f -> f.getType().equals(Short.class) || f.getType().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);
        }

        assertNull(fieldNamesById.put(key, name));
    });

Если вы хотитечтобы сообщить обо всех ошибках, попробуйте это:

List<String> problems = new ArrayList<>();

Map<Short, String> fieldNamesById = new LinkedHashMap<>();
Arrays.stream(InterfaceWithIds.class.getDeclaredFields())
    .filter(f -> f.getType().equals(Short.class) || f.getType().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);
        }

        String prevValue = fieldNamesById.put(key, name);
        if (prevValue != null) problems.add("key " + key + " mapped to " + name + " and " + prevValue);
    });

assertTrue(problems.toString(), problems.isEmpty());
0 голосов
/ 19 сентября 2018

С вашим реальным решением вы не очень далеко.
Вы можете положиться на groupingBy() и mapping() в первом наборе карт для сбора имен полей по значению поля.Таким образом, вам не нужен посредник Map.

Map<Short, List<String>> map = 
Arrays.stream(InterfaceWithIds.class.getDeclaredFields())
     .filter(f -> f.getType()
                   .getClass()
                   .equals(short.class))
     .map(f -> {
         Short key = null;
         String name = null;
         try {
             key = f.getShort(null);
             name = f.getName();
         } catch (IllegalAccessException e) {
             throw new RuntimeException(e);
         }
         return new AbstractMap.SimpleEntry<>(key, name);
     })
     .collect(groupingBy(SimpleEntry::getKey, LinkedHashMap::new, mapping(e -> e.getValue(), Collectors.toList())))
     .entrySet()
     .stream()
     .filter(e -> e.getValue()
                   .size() > 1)
     .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

assertThat(map, is(Collections.emptyMap()));
0 голосов
/ 19 сентября 2018

Вот поток, который группирует поля по статическому значению.Обратите внимание на некоторые комментарии о других изменениях / исправлениях

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())));

Метод, вызываемый для чтения значения, приведен ниже (в первую очередь предназначен для предотвращения блоков try / catch в потоке):

private static Short getValue(Field f) {
    try {
        return f.getShort(null);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...