为啥我得到一个仅在程序运行时偶尔出现的 NPE?

Posted

技术标签:

【中文标题】为啥我得到一个仅在程序运行时偶尔出现的 NPE?【英文标题】:Why Am I Getting An NPE That Only Appears Occasionally When The Program is Run?为什么我得到一个仅在程序运行时偶尔出现的 NPE? 【发布时间】:2015-03-04 11:22:13 【问题描述】:

我正在使用 BlueJ 中的 JUnit 为我的 GiftSelector 类编写一个测试类。当我运行testGetCountForAllPresents() 方法时,我得到一个NullPointerException 就行了:

assertEquals(true, santasSelector.getCountsForAllPresents().get(banana) == 3);

这个NPE的奇怪之处在于,我运行一次测试时很少出现,但我第二次运行测试时经常出现。有时直到我连续运行 7-8 次测试才会出现。

我收到的错误消息是: 没有异常消息。

GiftSelectortest.testGetCountForAllPresents 中第 215 行的 NPE

我的测试类的代码是:

import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * The test class GiftSelectorTest. The GiftSelector that you are 
 * testing must have testMode enabled for this class to function. 
 * This is done in the setUp() method.
 */
public class GiftSelectorTest

    private GiftList giftList1;
    private GiftList giftList2;
    private GiftList giftList3;
    private Child jack;
    private Child bob;
    private Child dave;
    private Child naughty1;
    private GiftSelector santasSelector;
    private Present banana1;
    private Present orange;
    private Present banana;
    private Present apple;
    private Present bike;
    private Present doll;
    private Present got;
    private Present pearlHarbour;
    private Present dog;
    private Present cat;
    private Present ball;
    private Present heineken;

    /**
     * Default constructor for test class GiftSelectorTest
     */
    public GiftSelectorTest()
    
        //Nothing to do here...
    

    /**
     * Sets up the test fixture.
     *
     * Called before every test case method.
     */
    @Before
    public void setUp()
    
        santasSelector = new GiftSelector();
        santasSelector.setTestMode(true);
        jack = new Child("Jack", 20, "1 A Place", true, true, true, false);
        bob = new Child("Bob", 10, "2 A Place", true, true, true, true);
        dave = new Child("Dave", 10, "3 A Place", true, true, true, true);
        naughty1 = new Child("John", 5, "4 A Place", true, true, true, true);
        giftList1 = new GiftList(jack);
        giftList2 = new GiftList(bob);
        giftList3 = new GiftList(dave);
        banana = new Present("banana", "fruit", 10);
        orange = new Present("orange", "fruit", 10);
        banana1 = new Present("banana", "fruit", 10);
        apple = new Present("apple", "fruit", 10);
        bike = new Present("bike", "toy", 200);
        doll = new Present("doll", "toy", 40);
        got = new Present("game of thrones", "dvd", 50);
        pearlHarbour = new Present("pearl harbour", "dvd", 20);
        dog = new Present("dog", "animal", 100);
        cat = new Present("cat", "animal", 80);
        ball = new Present("ball", "toy", 5);
        heineken = new Present("heineken", "beer", 1.60);
    

    /**
     * Tears down the test fixture.
     *
     * Called after every test case method.
     */
    @After
    public void tearDown()
    
        //Nothing to do here...
    


    @Test
    public void testGetCountForAllPresents()
    
        System.out.println(santasSelector.getCountsForAllPresents());
        //Test on empty GiftSelector
        assertNull(santasSelector.getCountsForAllPresents());

        //Test on a GiftSelector with one giftlist containing one present
        giftList1.addPresent(banana);
        santasSelector.addGiftList(giftList1);
        System.out.println(santasSelector.getCountsForAllPresents());
        assertEquals(true, santasSelector.getCountsForAllPresents().get(banana) == 1);

        //Test when GiftSelector contains 2 giftlists, each containing the same present object

        giftList2.addPresent(banana);
        santasSelector.addGiftList(giftList2);
        System.out.println(santasSelector.getCountsForAllPresents());
        assertEquals(true, santasSelector.getCountsForAllPresents().get(banana) == 2);

        //Test when GiftSelector contains 3 giftlists, 2 containing the same present object and another containing an identical present but with a different present instance
        giftList3.addPresent(banana1);
        santasSelector.addGiftList(giftList3);
        System.out.println(santasSelector.getCountsForAllPresents());
        assertEquals(true, santasSelector.getCountsForAllPresents().get(banana) == 3); //This is the line I get the NPE

        //Test when GiftSelector contains 3 giftLists, the first with one with a banana, the second with a banana and apple, and the third with a banana1 and ball
        giftList2.addPresent(apple);
        giftList3.addPresent(ball);
        System.out.println(santasSelector.getCountsForAllPresents());
        assertEquals(true, santasSelector.getCountsForAllPresents().get(banana) == 3);
        assertEquals(true, santasSelector.getCountsForAllPresents().get(apple) == 1);
        assertEquals(true, santasSelector.getCountsForAllPresents().get(ball) == 1);

    


    @Test
    public void testGetMostPopularPresent()
    
        //Test on empty GiftSelector
        assertNull(santasSelector.getMostPopularPresent());

        //Test on a GiftSelector with one giftList and one Present
        giftList1.addPresent(heineken);
        santasSelector.addGiftList(giftList1);
        assertEquals(true, santasSelector.getMostPopularPresent().comparePresent(heineken));

        //Tset on a GiftSelector with 1 giftList and 2 presents, one more expensive than the other
        giftList1.addPresent(banana);
        assertEquals(true, santasSelector.getMostPopularPresent().comparePresent(banana));

        //Test on a GiftSelector with 1 giftList and 3 presents. Banana and Apple are equal in price, and are both in the top3, 
        //therefore it should return the present closest to the start of the list
        giftList1.addPresent(apple);
        assertEquals(true, santasSelector.getMostPopularPresent().comparePresent(banana) || santasSelector.getMostPopularPresent().comparePresent(apple));

        //Test on a GiftSelector with 2 giftLists, the second list containing banana1, an indentical present to banana
        giftList2.addPresent(banana1);
        santasSelector.addGiftList(giftList2);
        assertEquals(true, santasSelector.getMostPopularPresent().comparePresent(banana));

        //Test on a GiftSelector with 2 giftLists, the first containing four presents and the second containing 2 presents.
        //This tests to see if top3 is working.
        giftList1.addPresent(bike);
        giftList2.addPresent(bike);
        assertEquals(true, santasSelector.getMostPopularPresent().comparePresent(bike));
    

我只包含了引用getCountsForAllPresents() 方法的测试方法。您会注意到我在每次调用包含getCountForAllPresents() 方法的assertEquals() 方法之前添加了打印语句。有趣的是,在我得到 NPE 的那一行之前,print 语句打印出 getCountForAllPresents() 返回的 HashMap 的正确值。

我注意到的另一个奇怪的事情是,当我使用 BlueJ 的内置调试器通过 testGetCountForAllPresents() 方法时,我注意到 giftList3 没有出现在 santaMap HashMapsantasSelector,但 print 语句仍然打印正确的计数,这意味着它必须知道 giftList3

getCountForAllPresents() 的代码是:

/**
 * For each present, calculate the total number of children who have asked for that present.
 * 
 * @return - a Map where Present objects are the keys and Integers (number of children requesting
 * a particular present) are the values. Returns null if santaMap is empty.
 */
public HashMap<Present, Integer> getCountsForAllPresents()

    if(!santaMap.isEmpty()) 
        //This HashMap contains a mapping from each unique real world present, represented by it's toComparisonString(), to a Present object representing it
        HashMap<String, Present> uniquePresents = new HashMap<String, Present>();
        //This HashMap contains a mapping from each Present object in uniquePresents to the number of times it's toComparisonString() is equal to another in santaMap
        HashMap<Present, Integer> presentFrequency = new HashMap<Present, Integer>();

         for(GiftList wishlist: santaMap.values()) 
            for(Present present: wishlist.getAllPresents()) 
                //Have we already seen this present?
                if(uniquePresents.containsKey(present.toComparisonString())) 
                    //If so, update the count in presentFrequency
                    Integer tmp = presentFrequency.get(uniquePresents.get(present.toComparisonString()));
                    tmp++;
                    presentFrequency.put(uniquePresents.get(present.toComparisonString()), tmp);
                 else 
                    //If not, add it to the maps uniquePresents and presentFrequency (with a frequency of 1)
                    uniquePresents.put(present.toComparisonString(), present);
                    presentFrequency.put(present, 1);
                
            
        
        //Return a map with unique presents as keys and their frequencies as values
        return presentFrequency;
    
    else 
        //If there are no mappings in Santa's map, return null
        return null;
    

我应该解释一下santaMap 是一个HashMap,其中Child 对象作为键,GiftList 对象作为值。它基本上将孩子映射到他们的圣诞愿望清单。 santaMap 只能包含同一个孩子的一个愿望清单。

我不知道为什么要获得 NPE,这与我编写 getCountForAllPresents() 方法的方式有关吗?我是如何实现测试方法/类的?

【问题讨论】:

可以添加Present的代码吗? 第 215 行在哪里得到空值? 我认为GiftSelector的完整代码可能是一个很好的补充 由于我删除了一些代码,所以行号将不对应。如果您查看 testGetCountForAllPresents() 方法,请转到倒数第二个代码块,您应该会看到行 assertEquals(true, santasSelector.getCountsForAllPresents().get(banana) == 3); 这就是我得到 NPE 的地方。它旁边应该有一条评论说“这是我获得 NPE 的地方” NPE + one-liners = 头痛。将您的代码分解为几行,以查看 NPE 实际命中的位置。不过我猜你的 santaMap 中有一个 Child 有一个 null GiftList 【参考方案1】:

您的Present 类不会覆盖hashCode()equals()。这意味着banana1banana 是任何HashMap 中的两个不同的键,将它们用作键。

那么让我们看看这里会发生什么。您有 bananabanana1 对象 - 第一个中的两个,第二个中的一个。

getCountsForAllPresents() 中,您有两个哈希映射。第一个是对象的比较字符串,第二个是对象本身。

添加遇到的第一个香蕉。如果它是 banana 对象,您将拥有如下内容:

独特的礼物
香蕉水果 10 ➞ [香蕉实例]

当前频率
[香蕉实例] ➞ 整数(1)

您继续迭代。您遇到下一个banana 对象。这是同一个对象。你会得到:

独特的礼物
香蕉水果 10 ➞ [香蕉实例]

当前频率
[香蕉实例] ➞ 整数(2)

现在您可以访问banana1 对象。这是一个不同的对象,但它具有相同的比较字符串!会发生什么?

此条件为真:uniquePresents.containsKey(present.toComparisonString())。这意味着它进入了if 的真实部分。

Integer tmp = presentFrequency.get(uniquePresents.get(present.toComparisonString()));

这意味着它将获取 banana-fruit-10 当前指向的对象,即 banana 对象 - 而不是 banana1 对象,获取其关联的频率,并增加它。它也由同一个对象存储。你现在拥有的是:

独特的礼物
香蕉水果 10 ➞ [香蕉实例]

当前频率
[香蕉实例] ➞ 整数(3)

请注意,presentFrequency 根本没有 banana1 键。现在你返回这个对象。

当您尝试通过 banana 检索时,它可以正常工作 - 断言有效。

但请记住,santaMap 本身就是 HashMap。这意味着它没有保证订单。迭代器可能会给你giftList1,giftList2,giftList3,但它也可能给你giftList3,giftList1,giftList2 - 或任何其他顺序。

那么当它首​​先给你giftList3 时会发生什么?你最终会得到:

独特的礼物
香蕉水果 10 ➞ [banana1 实例]

当前频率
[banana1 实例] ➞ 整数(3)

为什么?因为banana1 是第一个带有密钥banana-fruit-10 的礼物,从现在开始就是这样。

发生这种情况时,当您尝试从返回的对象中获取banana 时,频率列表中不存在该键。它返回null - 还有你的NullPointerException

【讨论】:

感谢您如此清楚地解释这一点。你有什么办法可以解决这个问题,让它总是返回正确的答案吗?我对编程还很陌生,所以我还没有介绍方法覆盖和其他 OO 功能,有没有比在 Present 中覆盖 hashCode() 和 Equals() 更好的解决方案? 好吧,如果您打算将这些对象用作哈希映射中的键,则不会。但也许您可以使用其他东西,例如包含哈希映射(由比较字符串作为键)的对象,并具有将 Present 对象作为参数的 get 方法,并从内部哈希映射返回频率通过它的比较字符串。然后将该对象用作getCountsForAllPresents() 的返回类型。

以上是关于为啥我得到一个仅在程序运行时偶尔出现的 NPE?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 InputStreamReader 从 jar 读取时会抛出 NPE?

为啥我在更改屏幕并在 Libgdx 中绘制 Sprite 后得到 NPE

为啥我的 Android 应用程序偶尔会非常快地耗尽电池电量?

为啥我的 iOS 应用程序仅在首次运行时正确检测到当前语言?

启动 JavaFX 应用程序时的内部 NPE

VS2005(C#)编写Web Service 运行在客户那边偶尔出现IIS错误,现在不知道问题,希望得到高手的指点