与流API交换实现后,Junit测试失败,为什么?

时间:2019-04-17 18:29:38

标签: java-8 hashmap java-stream

我实现了以下方法,概述了StringMap<String, List<String>>的值中出现的情况:

public static Map<String, Long> getValueItemOccurrences(Map<String, List<String>> map) {
    Map<String, Long> occurrencesOfValueItems = new HashMap<>();

    map.forEach((key, value) -> {
        value.forEach(item -> {
            if (occurrencesOfValueItems.containsKey(item)) {
                occurrencesOfValueItems.put(item, occurrencesOfValueItems.get(item) + 1);
            } else {
                occurrencesOfValueItems.put(item, 1L);
            }
        });
    });

    return occurrencesOfValueItems;
}

我已经用一个JUnit测试对它进行了测试,测试成功了。在这里(现在还包括进口):

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

class TryoutTest {

    static Map<String, List<String>> items = new HashMap<>();
    static List<String> largeList = new ArrayList<String>();
    static List<String> mediumList = new ArrayList<String>();       
    static List<String> smallList = new ArrayList<String>();
    static List<String> differentLargeList = new ArrayList<String>();
    static List<String> differentSmallList = new ArrayList<String>();
    static List<String> anotherList = new ArrayList<String>();
    static List<String> someList = new ArrayList<String>();
    static List<String> justAList = new ArrayList<String>();

    @BeforeAll
    static void setup() {
        largeList.add("Alfred");
        largeList.add("Bakari");
        largeList.add("Christian");
        largeList.add("Dong");
        largeList.add("Etienne");
        largeList.add("Francesco");
        largeList.add("Guido");
        largeList.add("Henrik");
        largeList.add("Ivan");
        largeList.add("Jos");
        largeList.add("Kumar");
        largeList.add("Leonard");
        largeList.add("Marcin");
        largeList.add("Nico");
        largeList.add("Olof");
        items.put("fifteen-01", largeList);

        mediumList.add("Petar");
        mediumList.add("Quentin");
        mediumList.add("Renato");
        mediumList.add("Sadio");
        mediumList.add("Tomislav");
        mediumList.add("Ulrich");
        mediumList.add("Volkan");
        mediumList.add("Wladimir");
        items.put("eight-01", mediumList);

        smallList.add("Xavier");
        smallList.add("Yves");
        smallList.add("Zinedine");
        smallList.add("Alfred");
        items.put("four-01", smallList);

        differentLargeList.add("Bakari");
        differentLargeList.add("Christian");
        differentLargeList.add("Dong");
        differentLargeList.add("Etienne");
        differentLargeList.add("Francesco");
        differentLargeList.add("Xavier");
        differentLargeList.add("Yves");
        differentLargeList.add("Wladimir");
        differentLargeList.add("Jens");
        differentLargeList.add("Hong");
        differentLargeList.add("Le");
        differentLargeList.add("Leigh");
        differentLargeList.add("Manfred");
        differentLargeList.add("Anders");
        differentLargeList.add("Rafal");
        items.put("fifteen-02", differentLargeList);

        differentSmallList.add("Dario");
        differentSmallList.add("Mohammad");
        differentSmallList.add("Abdul");
        differentSmallList.add("Alfred");
        items.put("four-02", differentSmallList);

        anotherList.add("Kenneth");
        anotherList.add("Hong");
        anotherList.add("Bakari");
        anotherList.add("Ulrich");
        anotherList.add("Henrik");
        anotherList.add("Bernd");
        anotherList.add("Samuel");
        anotherList.add("Ibrahim");
        items.put("eight-02", anotherList);

        someList.add("Kumar");
        someList.add("Konrad");
        someList.add("Bakari");
        someList.add("Francesco");
        someList.add("Leigh");
        someList.add("Yves");
        items.put("six-01", someList);

        justAList.add("Bakari");
        items.put("one-01", justAList);
    }

    @Test
    void valueOccurrencesTest() {
        Map<String, Integer> expected = new HashMap<>();
        expected.put("Abdul", 1);
        expected.put("Alfred", 3);
        expected.put("Anders", 1);
        expected.put("Bakari", 5);
        expected.put("Bernd", 1);
        expected.put("Christian", 2);
        expected.put("Dario", 1);
        expected.put("Dong", 2);
        expected.put("Etienne", 2);
        expected.put("Francesco", 3);
        expected.put("Guido", 1);
        expected.put("Henrik", 2);
        expected.put("Hong", 2);
        expected.put("Ibrahim", 1);
        expected.put("Ivan", 1);
        expected.put("Jens", 1);
        expected.put("Jos", 1);
        expected.put("Kenneth", 1);
        expected.put("Konrad", 1);
        expected.put("Kumar", 2);
        expected.put("Le", 1);
        expected.put("Leigh", 2);
        expected.put("Leonard", 1);
        expected.put("Manfred", 1);
        expected.put("Marcin", 1);
        expected.put("Mohammad", 1);
        expected.put("Nico", 1);
        expected.put("Olof", 1);
        expected.put("Petar", 1);
        expected.put("Quentin", 1);
        expected.put("Rafal", 1);
        expected.put("Renato", 1);
        expected.put("Sadio", 1);
        expected.put("Samuel", 1);
        expected.put("Tomislav", 1);
        expected.put("Ulrich", 2);
        expected.put("Volkan", 1);
        expected.put("Wladimir", 2);
        expected.put("Xavier", 2);
        expected.put("Yves", 3);
        expected.put("Zinedine", 1);
        assertThat(FunctionalMain.getValueItemOccurrences(items), is(expected));
    }
}

当我将方法的实现更改为

public static Map<String, Long> getValueItemOccurrences(Map<String, List<String>> map) {
    return map.values().stream()
            .flatMap(Collection::stream)
            .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
}

测试用例失败,指出结果图不等于预期的图。请看这张eclipse屏幕快照,该屏幕截图显示,显然,元素的顺序使测试失败:

enter image description here

是真的吗?我想我已经读过HashMap通常不能保证键的顺序。

我的问题(很长)是:我该怎么做才能使流API调用产生通过测试的结果,或者我必须更改测试用例,也许使用其他断言?

一些子问题是:

  • 是否有另一种/更好的方法可以将流API用于此方法?
  • 如果订单很重要(也许Map,我是否必须返回特定的TreeMap实现?

2 个答案:

答案 0 :(得分:5)

TL; DR ,您的测试已损坏,请解决此问题。

首先,使用以下方法更容易重现:

List<String> list = ImmutableList.of("Kumar", "Kumar", "Jens");

public static Map<String, Long> getValueItemOccurrences1(List<String> list) {
    return list
        .stream()
        .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
}

public static Map<String, Long> getValueItemOccurrences2(List<String> list) {
    Map<String, Long> occurrencesOfValueItems = new HashMap<>();

    list.forEach(item -> {
        if (occurrencesOfValueItems.containsKey(item)) {
            occurrencesOfValueItems.put(item, occurrencesOfValueItems.get(item) + 1);
        } else {
            occurrencesOfValueItems.put(item, 1L);
        }
    });

    return occurrencesOfValueItems;
}

问题在于,在内部HashMap::hash(也称为重新哈希)之后,在决定选择哪个存储桶时获得实际上重要的最后一位,它们具有相同的值:

    System.out.println(hash("Kumar".hashCode()) & 15);
    System.out.println(hash("Jens".hashCode()) & 15);

简单地说,HashMap根据条目hashCode决定将条目放置在何处(选择了存储桶)。好吧,差不多,一旦计算出hashCode,内部就会完成另一个hash -以更好地分散条目。 int的最终hashCode值用于确定存储桶。当您创建默认容量为16(例如,通过new HashMap的HashMap时,只有最后4位与条目的去向无关紧要(这就是我在此处进行& 15的原因) -查看最后4位)。

其中hash是:

// xor first 16 and last 16 bits
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

现在,事实证明,在应用上述算法后,["Kumar" and "Jens"] ["Xavier", "Kenneth", "Samuel"]的后4位相同(第一种情况下为3,第二种情况下为1)情况)。

现在我们知道了这些信息,实际上,这可以进一步简化:

Map<String, Long> map = new HashMap<>();
map.put("Kumar", 2L);
map.put("Jens", 1L);

System.out.println(map); // {Kumar=2, Jens=1}

map = new HashMap<>();
map.computeIfAbsent("Kumar", x -> 2L);
map.computeIfAbsent("Jens", x -> 1L);
System.out.println(map); // {Jens=1, Kumar=2}

我使用过map.computeIfAbsent,因为Collectors.groupingBy是在引擎盖下使用的。


事实证明,putcomputeIfAbsent以不同的方式将元素放入HashMap中;完全允许这样做,因为Map始终没有任何顺序-而且这些元素无论如何最终都位于同一存储桶中,这是导入部分。因此,逐项测试您的代码,先前的测试代码已损坏。


如果您愿意的话,这甚至更有趣:

HashMap::put将以Linked的方式添加元素(直到创建Tree条目),因此,如果存在一个元素,则将添加所有其他元素,如:

one --> next --> next ... so on.

元素在进入end of this queue方法时会附加到put

另一方面,computeIfAbsent有所不同,它将元素添加到队列的开头。如果我们以上面的示例为例,则首先添加Xavier。然后,当添加Kenneth时,成为第一个:

 Kenneth -> Xavier // Xavier was "first"

添加Samuel后,它将成为第一个:

 Samuel -> [Kenneth -> Xavier] 

答案 1 :(得分:0)

我强烈建议您开始使用AssertJ而不是JUnit的内置断言。 这样做,您可以使用以下AssertJ断言:

assertThat(FunctionalMain.getValueItemOccurrences(items))containsOnly(expected);

containsOnly()检查您的地图以任何顺序具有完全相同的元素

除此优势外,AssertJ的assertThat()还利用了流畅的语法(与JUnit的内置assertThat不同),因此您的IDE可以为您提供上下文相关的帮助,以使您知道AssertJ的数百种特定于类型的断言中的哪些是可用于您的测试值。