如何从流计算Map,然后检查Map值的属性?

时间:2018-09-19 12:58:01

标签: java java-8 hashmap java-stream

我的要求:我有一个仅包含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()));
}

是否有避免该中间本地地图实例的方法?

(额外的问题:是否有更好的方法来缩短用键/值对填充地图的lambda?)

5 个答案:

答案 0 :(得分:4)

这是一个按静态值对字段进行分组的流。请注意有关其他更改/更正的一些评论

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

答案 1 :(得分:4)

如果您希望有效地进行此检查(通常不需要太多的单元测试),则可以通过乐观地假定字段没有重复项并首先执行廉价的预测试来减少工作量。此外,您可以使用此预测试的结果来获取不带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: ", "")));
}

它首先为所有遇到的值填充一个位集,并为多个遇到的值填充另一个位集。如果后面的位为空,我们将立即返回,因为没有重复项。否则,我们可以将该位集用作廉价过滤器,以获取具有问题值的字段。

答案 2 :(得分:2)

与您的实际解决方案相距不远。
您可以依靠第一个地图收集中的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()));

答案 3 :(得分:2)

不确定是否可以满足您的需求,但为什么不简单:

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

如果有重复项,将失败并显示异常。赶上并做Assert.fail(...)

我希望我输入的代码正确无误,在电话上键入

答案 4 :(得分:1)

这里有一些问题。首先,f.getClass()将为您提供Field实例的类,而不是字段的实际类。您要

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

接下来,您需要记住Short.classshort.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());