Java中一些 NPE 报错和定位办法整理


=Start=

缘由:

已经有很长一段时间都没有写Java代码了,最近因为工作需要又要再用Java实现一些功能,因为Java基础的不够扎实以及生产环境的复杂多变导致本地运行测试OK的代码到了线上之后就会遇到一些非预期的报错,最常见的就是NullPointerException空指针异常了。因此这里及时整理总结一下近期遇到的NPE异常及其解决办法,方便后面有需要的时候参考。

正文:

参考解答:

Java中的NPE问题对于我这种Java新手来说真的是太常见了……很烦,不过有一点好就在于报错的行定位非常精准,问题肯定就出在那个地方了,只是根因不一定在那(有可能是之前某个地方处理不妥善最后流转到这里来了),这时需要一层一层的往上定位了。

整理总结一下最近经常遇到的几个 NPE 的点:

1、json中给一个key的value赋值为null或空串其效果是不一样的

  • 在 fastjson 中当一个key的value内容是null时,最后其实是没有这个 key 的,这点需要注意一下,特别是在后期有处理这个json对象的操作时;
  • 在 fastjson 中当一个key的value内容是””时(空字符串),后续处理的时候会存在这个 key ,且内容为””,符合预期。

2、从json中(链式)取不存在的 key ;

首先,要处理的字符串必须符合json的格式要求,否则会报 JSONException 异常,这个比较简单,但也需要确认一下;
其次,如果一层一层处理的话,如果哪一层没有对应的 key 则在那一层返回的就是 null ,所以下层如果继续处理的话就会报 NullPointerException 异常;
最后,如果希望能正确解析json字符串,需要对json字符串的格式有充分了解(有哪些字段,字段对应的是什么类型),符合预期就正确处理,不符合预期抛出异常或是报错即可。

以下是一个测试用的代码片段,方便进行本地验证:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

import java.io.IOException;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

public class DoTest {
    public static void main(String[] args) {
        String input = "{\"msg\":\"success\",\"code\":\"200\",\"data\":\"{\\\"score\\\":3,\\\"key1\\\":3,\\\"key2\\\":2,\\\"tags\\\":\\\"a,b,c\\\"}\"}";
        input = "{\"msg\":\"success\",\"code\":\"200\",\"data\":\"{\\\"score\\\":0}\"}"; // 0 null
        input = "{\"msg\":\"success\",\"code\":\"200\",\"data\":\"{\\\"key1\\\":0}\"}"; // null null
        input = "{\"msg\":\"success\",\"code\":\"200\",\"data\":\"{}\"}"; // null null
        // input = "{\"msg\":\"success\",\"code\":\"200\",\"data\":\"\"}"; // NullPointerException
        // input = "{\"msg\":\"success\",\"code\":\"200\",\"data\":\"\""; // JSONException: syntax error, position at 0, name data
        // input = "{\"msg\":\"success\",\"code\":\"200\"}"; // NullPointerException
        JSONObject inputJson = JSON.parseObject(input);
        System.out.println(inputJson);
        Integer score = inputJson.getJSONObject("data").getInteger("score");
        String tagStr = inputJson.getJSONObject("data").getString("tags");
        System.out.println(score + "\t" + tagStr);

        Set<String> tags = new HashSet<>();
        // tags.add("111");
        // tags.add("112");
        System.out.println(tags.toString());

        JSONObject result = new JSONObject();
        result.put("1001", 5);
        System.out.println(result);
        System.out.println(result.getInteger("1001"));

        Integer int1001 = result.getInteger("1001");
        int sum = 0;
        if (sum > 0 || (int1001 != null && int1001 > 0)) {
            sum += int1001;
            result.put("score", sum>100?100:sum);
        }
        System.out.println(result);

        // result.remove("1001");
        System.out.println(result);

        Integer ret = 0;
        ret += result.getInteger("1001");
        System.out.println(ret);

        Properties prop = new Properties();
        try {
            //load a properties file from class path, inside static method
            prop.load(DoTest.class.getClassLoader().getResourceAsStream("config.properties"));

            //get the property value and print it out
            System.out.println(prop.getProperty("json").length());
        }
        catch (IOException ex) {
            ex.printStackTrace();
        }

    }
}

3、在不同类型的变量之间进行操作;

以 int 和 Integer 为例:
int 是基本类型,不是类,为了符合面向对象编程,后来出现了Integer类,它是对int进行封装的。
int 不是对象,是java原始的数据类型,它默认值为0
Integer 是对象,它是对int进行的封装,它默认值为NULL
建议在用 Integer 时,注意检查其值是否为 null ,避免 NPE 报错。

Integer sum = new Integer(0);

4、json取值中 getInteger 和 getIntValue 两者之间的区别和试用场景;

一般情况下(只需要取key对应整数的数值大小时),我个人还是建议用 getIntValue 因为它返回的是 int ,没取到返回 0 ;如果对于内容可能为 0 的那些场景,可以用 getInteger 再加上 null 的判断;或者统一都用getInteger+null判断。

JSONObject jsonObject = new JSONObject();
String data = null; // {}
jsonObject.put("data", data);

System.out.println(jsonObject.getInteger("key1")); // null
System.out.println(jsonObject.getIntValue("key1")); // 0
System.out.println(jsonObject); // {}

data = ""; // {"data":""}
jsonObject.put("data", data);
System.out.println(jsonObject); // {"data":""}

jsonObject.put("key1", 1);
System.out.println(jsonObject.getInteger("key1")); // 1
System.out.println(jsonObject.getIntValue("key1")); // 1
System.out.println(jsonObject); // {"key1":1,"data":""}

5、Apache POI中的 setCellValue 操作;

用Apache POI操作时,createCell 创建最多报 IllegalArgumentException 异常,不会有 NPE 异常,那个只会出现在 setCellValue 设置单元格内容时出现,原因就是设置的值类型为 null ,而且简单测可能还不容易发现,比如:

import com.alibaba.fastjson.JSONObject;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileOutputStream;
import java.io.IOException;

public class WriteExcel {
    public static void main(String[] args) {
        XSSFWorkbook workbook = new XSSFWorkbook();
        XSSFSheet sheet = workbook.createSheet("ixyzero");

        int rowCount = 0;
        Row row = sheet.createRow(++rowCount);

        JSONObject jsonObject = new JSONObject();

        int columnCount = 0;
        Cell cell = row.createCell(columnCount++);
        System.out.println(jsonObject.getIntValue("key1")); // 0
        cell.setCellValue(jsonObject.getIntValue("key1")); // 正常,因为即便取不到key1的value会默认返回0

        String data = null;
        cell = row.createCell(columnCount++);
        cell.setCellValue(data); // 它说他对null做了容错处理,不会报 NPE 异常

        cell = row.createCell(columnCount++);
        System.out.println(jsonObject.getInteger("key1")); // null
        cell.setCellValue(jsonObject.getInteger("key1")); // NullPointerException

        try (FileOutputStream outputStream = new FileOutputStream("ixyzero.xlsx")) {
            workbook.write(outputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

参考链接:

java中因为 int 和 Integer 不同导致的 NPE 问题
https://stackoverflow.com/questions/218384/what-is-a-nullpointerexception-and-how-do-i-fix-it
https://blog.csdn.net/weixin_41575100/article/details/78953079

https://stackoverflow.com/questions/17487205/how-to-check-if-a-json-key-exists

https://stackoverflow.com/questions/14283684/null-check-for-hashmap-key

http://poi.apache.org/apidocs/dev/org/apache/poi/ss/usermodel/Row.html#createCell-int-

http://apache-poi.1045710.n5.nabble.com/Bug-59199-New-setting-null-value-to-date-cell-leads-to-NPE-td5722365.html

https://www.experts-exchange.com/questions/23180270/How-to-avoid-NullPointerException-in-excel-writing.html

https://stackoverflow.com/questions/20694915/apachi-poi-cell-setcellvalue-thows-nullpointerexception

=END=


《 “Java中一些 NPE 报错和定位办法整理” 》 有 2 条评论

  1. 万恶的NPE差点让我半个月工资没了
    https://mp.weixin.qq.com/s/xoOalilXsmeOcvpjf24Z9g
    `
    最近看到《阿里巴巴Java开发手册》第11条规范写到:
    防止 NPE ,是程序员的基本修养

    NPE(Null Pointer Exception)一直是开发中最头疼的问题,也是最容易忽视的地方。记得刚开始工作的时候所在的项目组线上出现最多的bug不是逻辑业务bug而是NPE,所以后面项目组出了一个奇葩的规矩,线上如果谁出现一个NPE的问题就罚款100元,用作团建费用。如果项目组每个人一个月都出现个两三个NPE的话。那么项目组是不是每个月都可以去团建下(自己掏钱海吃海喝,心不心疼)。不过自从这个规矩实施以来,线上的NPE就渐渐的少了,从最初的一个月团建一次到最后的半年团建一次。大家写代码都比较谨慎了,只要用到对象或者集合的时候二话不说上来先判空,所以产生的NPE就少了。

    # 那么如何有效的避免NPE呢?
    * 使用对象或者集合之前记得先判空。
    * 使用JDK一些API的方法记得要点进源码去大概看看,不要随便拿来就用。
    * 单元测试要对空值进行测试,保证程序的健壮性。
    * 合理的使用JDK1.8提供的Optional来避免NPE。
    * 提供接口时候需要对非空参数进行说明,并且对非空参数进行校验,不要太相信调用者。
    * 调用接口的时候一定要对接口返回值进行判空,不要太相信接口提供者。(这个肯定会有值的)。
    * 小心使得万年船
    `

  2. 我有一个朋友写出了17种触发NPE的代码!避免这些坑
    https://mp.weixin.qq.com/s/VGH0pDbsgP5OuX8x7HyuKg
    `
    虽然无法统计出Java程序员写的最多的异常是哪一个,NullPointerException(NPE)它一定会榜上有名。无论是初次涉足编程世界的新手,还是在代码海洋中久经风浪的老鸟,几乎都写过NPE异常。要防范它,不在高超的编码技巧,在细。

    我有一个朋友,写代码的时候常常遭到NPE背刺,痛定思痛,总结了NPE出没的17个场景,哪一个你还没有遇到过?

    1.访问空对象的实例变量或者调用空对象的实例方法

    从其它方法(如远程hsf方法或者从DB)返回的结果,不做控制判断,直接访问变量或者方法,可能会NPE。

    2.访问或修改空数组的元素

    3.未初始化的对象数组中的元素默认是null

    4.throw一个null 出去

    在Java中,通常不会故意使用 throw null 这种表达,因为它实际上没有任何有用的用途。根据Java语言规范,throw 关键字后面应该跟随一个可抛出的对象(Throwable 类或其子类的实例),而 null 并不是一个可抛出的对象。如果你执行 throw null ,将会得到一个NPE。

    猜想可能出现的场景:

    代码错误:throw null 可能是代码编写错误或者不完整的异常处理。例如,可能打算抛出一个实际的异常对象,但误写成了 null。

    测试代码:在单元测试中,有时可能会故意使用throw null , 来确保他们的异常处理代码能够妥善处理意外情况。(不推荐)

    5.在null 引用上进行synchronized同步

    6.在自动拆箱过程中遇到null

    自动装箱不会遇到Null的问题,因为这个过程是把基本类型的值转换成包装类对象,基本类型的值是没有Null的。

    定义方法返回一个int 作为出参,但实际return 一个null,也会NPE。

    7.从集合/Map中获取null元素并直接使用

    从集合/map中获取元素并使用时,建议对Null进行检查,以避免潜在的NPE,特别是在那些隐式触发自动拆箱的场景中。

    8.方法链调用中上一步骤返回null

    9.枚举的valueOf方法使用null

    10.集合操作不支持null元素

    HashSet 、LinkedHashSet 都只允许添加一个null。后续无论添加多少null元素,都会被忽视。

    TreeSet 不允许添加null值,排序集合依赖元素直接的比较操作,而null元素不能与其它对象进行比较,会抛出NPE;

    11.多线程环境中无适当同步可能导致不一致状态

    12.依赖注入:注入的对象为null

    required属性为false,启动过程中如果没有找到合适的bean,service 会被设置为null。在调用service的任何方法之前都需要判断service是否为null。

    13.Lambda表达式或方法引用中目标引用为null

    14.Stream API处理时遇到null元素

    15.使用增强for循环遍历集合时,没有判空

    16.在Junit中使用Mockito时,错误地使用any()匹配基本数据类型参数

    在JUnit4中,使用Mockito框架时,any() 是一个参数匹配器,当与基本数据类型一起使用时,需要使用相应的类型特定的匹配器,例如使用anyInt() 而不是any()。因为any()实际上返回的是null,而null不能自动转换为基本数据类型。

    17.Optional类的正确使用

    Optional类在 Java 8 中被引入,其设计初衷是为了提供一种更优雅的方式来处理可能为Null的值,从而减少空指针异常NPE的发生。

    但Optional类设计为减少NPE的可能性,却并不是万能的,比如开发者在使用Optional,不检查是否存在,直接调用Optional.get(),那么会得到一个NoSuchElementException。也仍然存在即使使用了Optional,也可能出现NPE的情况。
    `

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注