Home 编程规范

有人说,防止NullPointerException,是程序员的基本修养。

此篇博文,将介绍空指针的相关知识,一起往下看看吧~

 

小杨是一名初入职场的程序员,他很喜欢写代码。每次产品需要他实现的功能,他都能做出来,可当他信心满满的把代码交给组里前辈CR的时候,前辈总会语重心长的告诉他:

小杨, 你这代码用不了啊,可能会报空指针异常,诺,就是这个

此时,小杨还没意识到事情的严重性,他想:这太正常了,谁写个代码还不会出错呢,修复就行了

public class InterToInt{
    public static void main(String[] args) {
        Person person = new Person();
        person.setName("张三");
        Integer count = person.getCount();
        if (count == 0) {
            System.out.println("该变量未填写count");
        }
        // 装箱操作 int -> Integer
        int i = 5;
        Integer integer = new Integer(i);

        // 拆箱操作:Integer -> int
        int num = integer.intValue();
    }
}

上述代码,究竟为什么会出现空指针异常呢?
经过调试,同时结合报错信息可以判断,问题出在if(count == 0)这条语句上,仔细看,我们并没有对person对象的count属性进行赋值,难道是因为count默认值为null,才报的空指针异常吗?

由上图可知,Integer类型的默认值确实为Null,好,那我们给count赋值,这样的话就不会报空指针了吧

果然,没有报空指针错误了,难道问题到这里就解决了吗?未必
在开发中,有时候对于一个属性,我们不需要给其赋值,它也不需要默认值,它就需要用Null来表示。阿里巴巴开发手册规定所有的属性必须用Integer来定义而非int
借助上例,如果我们从库表里面查询一个person对象xiaohuang,xiaohuang的count属性正好是Null,难道我们能给它随便赋一个值吗?显然不能
那我们怎么解决空指针的问题呢?
public class InterToInt{
    public static void main(String[] args) {
        Person person = new Person();
        person.setName("张三");
        Integer count = person.getCount();
        /**拆箱的时候实际上是调用了intValue()方法。
         * Integer类型和int作比较会自动拆箱,由于Interger类型的对象是null,
         * 这时候自动拆箱调用intValue()方法就会报NullPointerException。
         * 所以基本类型和对应包装类作比较时要判断包装类是否有可能为null
         * 不然就会出现这种错误
         */
        if (count != null && 1 == count) { //改写这一句即可
            System.out.println("count == 0");
        }
        // 装箱操作 int -> Integer
        int i = 5;
        Integer integer = new Integer(i);

        // 拆箱操作:Integer -> int
        int num = integer.intValue();
    }
}

到这里,是不是似懂非懂,若有若无?如果你以为你每次在使用count这样的属性前进行非空判断就能避免所有的空指针的话,那就太天真了,空指针要比这狡猾的多,接下来我们一起去揭开空指针的神奇面纱吧~

一、Java中,什么是指针?

Java中究竟有没有指针?你可以就这个问题好好探索下,此处不作深究,本文中的指针可以理解为指针对象

String str = new String(“str是指针对象”);

如上,str就是一个指针,它被存放在Jvm的栈区;留几个问题给你思考吧~ 1.new String(“str是指针对象”)的空间是什么时候分配的? 2.new String(“str是指针对象”)存储在Jvm的哪一块区域?

 

二、什么是空指针?

当指针不指向任何内存地址时,就叫做空指针,例如:int[] array = null

 

三、什么叫空指针异常?

就是一个指针不指向任何内存地址,但是你还调用他了,例如:

int[] array = null;
System.out.println(array[0]);
这个时候原本array数组是个空指针,没有创建新的对象,在调用这个数组的时候就会产生空指针异常的错误!
程序运行会显示Exception in thread “main” java.lang.NullPointerException的异常提示

四、为什么会产生空指针异常呢?

这里我们用上面举的例子进行说明,int[] array = null在内存中的栈内存中创建了一个叫做array的变量,而堆内存中并没有开辟int类型的数组空间,所以在栈内存中的这个array变量没有存放任何内存地址,由此我们可以理解为什么会产生空指针异常,调用没有的东西显然时不可以的。(栈区和堆区之间存放的东西有什么关系呢?详情可在本博文搜索Jvm、栈、堆等关键词)

 

四、NullPointerException以及其产生的场景

1.产生NullPointerException的场景

Java中定义:在应用程序中尝试使用null时会抛出异常。

其中以下的情况会产生NullPointerException

  1. 调用 null 对象的实例方法。
  2. 访问或修改 null 对象的字段。
  3. 将 null 作为一个数组,获得其长度。
  4. 将 null 作为一个数组,访问或修改其时间片。
  5. 将 null 作为 Throwable 值抛出。

 

2.怎么产生的?

《阿里巴巴开发手册》中提到,

1)返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。反例:public int f() { return Integer 对象}, 如果为 null,自动解箱抛 NPE。

2) 数据库的查询结果可能为 null。

3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。

4) 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。

5) 对于 Session 中获取的数据,建议 NPE 检查,避免空指针。

6) 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。正例:使用 JDK8 的 Optional 类来防止 NPE 问题。

 

3.怎么预防NPE?

1) 对象防止,直接!=null

2)集合类判空:一般采用!=null&&判断size(),或者调用isEmpty()方法,或者用Collection工具类判空,java8种Optional类

3)字符串判空:需要判断是否==null&&””.equals(str)来判断,或者StringUtils工具类判断

4)另外项目中要对所有前台参数,对象判空,数据库查询语句判空,JSON对象,JSON数组判空,get()后的值判空

5)对于级联调用 obj.getA().getB().getC(),可以使用Optional类

6)主动进行参数检查,对方法中传入的参数进行检验

7)在已知字符串上使用equals(),equalsIgnoreCase()等方法

8)尽量避免方法中返回null。一些返回数组或者List的方法,如果没有值,尽量返回空集合,避免返回null


总结

记住一句话:避免空指针异常的最好的方法就是总是检查哪些不是自己创建的对象。


补充

针对第一点,我们进行实验

定义Person类

定义测试类

我们对count进行了非空判断,可它还是报了空指针异常,错误信息帮我们定位到第25行

public int getCount(){
    return this.count;
}

这是因为 
返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。反例:public int f() { return Integer 对象}, 如果为 null,自动解箱抛 NPE。
拆箱的时候实际上是调用了intValue()方法。Integer类型和int作比较会自动拆箱,由于Interger类型的对象是null,这时候自动拆箱调用intValue()方法就会报NullPointerException。
所以要判断包装类是否有可能为null,不然就会出现这种错误
class VerdictInter{
    public static void main(String[] args) {
        Person person = new Person();
        person.setName("张三");
        person.setCount(null);
        Integer count = person.getCount();
        int res = (count == null) ? 0:count;
        if (res == 0) {
            System.out.println("该变量未填写count");
        }
    }
}


参考资料:
防止NullPointerException,是程序员的基本修养 - 知乎 (zhihu.com)

空指针异常--java.lang.NullPointerException - 云+社区 - 腾讯云 (tencent.com)

(36条消息) java中什么是空指针异常以及为什么会产生空指针异常_imagpie的博客-CSDN博客_为什么会出现空指针异常

0 107

良好的编程习惯从准确,有效,容易辨别的变量名定义开始;

最重要的注意事项

该变量名要完全,准确地描述出该变量所代表的事物;

以问题为导向;

最适当的名字长度;(10到16)

变量名对作用域的影响:短变量名一般适用于循环或局部,代表作用域十分有限;

限定词加到最后:expenseTotal,revenueTotal;具有优雅的对称性;

特定类型的数据命名

为循环下标命名

i,j,k只用于循环当中,如果作用范围比此大,请使用其他命名;

为状态变量命名

应当直接读懂,而非简单的flag,如isOpendoor;

为临时变量命名

临时变量也应该提供信息,如temp则无法表示具体意义,改为Combined(合);

为布尔变量命名

done,error,found,success;

注意,不要使用否定的布尔变量 如notOpen,可读性差且需要双重否定;

为枚举类型命名

使用前缀标明为一个组,如Color_red,Color_blue;

为具名变量命名

应该使用其意义,而非其数字,如CYCLES_NEEDED比FIVE好;

命名规则的作用

要求你更多的按规则行事,通过做一项全局决策而不是多次局部决策,你可以集中精力关注其他的事情;

有助于阅读项目代码和其他人的交流;

有助于学习新的代码项目;

有助于减少名字增生;

弥补编程语言的不足之处;

强化相关变量之间的关系;

总结:规则为你的代码增加了结构,减少了你要考虑的事情

变量命名指导原则

在代码里缩写对照表来解释变量含义;

编写一份标准文档;

应当避免的名字

避免使用令人误解的名字和缩写;

避免使用具有相似含义的名字,如input和inputValue;

避免使用拼写相近的名字,如gad和good;

避免使用发音相近的名字;

避免在名字中使用数字;

避免拼错单词;

避免使用多种语言标准;

避免使用关键字作为名字,如 if  if then then;

要点

对于变量的命名要多加考虑;

名字要尽可能的具体;

命名规则应该可以区别局部数据,类数据,和全局数据;

名字应该方便阅读!!!

0 82

编程并不是一件难事,但是如何让编程更优雅,的确需要一些技巧。

在查找资料以及做leetcode题目的时候,我常常发现自己的代码十分臃肿,不易阅读,不易调试,因此我决心改变自己的编程习惯,通过《代码大全》这本书,使我受益良多,因此写下这篇博客。

变量定义

隐式声明(使用未声明变量时,编译器会自动为你声明)

隐式声明变量对于任何语言来说都是最危险的特性之一;十分容易发生变量混淆,例如需要使用acctno,却错写成acctnum;

建议:关闭编译器的隐式声明;

建议:声明全部的变量;

建议:遵循某种命名规则;(如:employeeNum,employeeName)

建议:检查变量名;(使用插件或编译器自带)

优点:避免变量混淆,提醒自己更加清晰的使用变量;

变量初始化原则

不合理的初始化是产生编程错误的常见根源之一,掌握初始化原则可以节省许多的调试时间;

       建议:在声明变量的时候初始化;

            int a=5;

建议:在靠近变量第一次使用的位置初始化它;

                                   int  a=5;

                                   code using a;

                                   int b=10;

                                   code using b;

建议:使用final,const确保该变量初始化之后再被赋值;

建议:注意计数器和累加器的变量重置问题;

建议:在类的构造函数里面初始化该类的数据成员;

建议:检查是否需要重新初始化;(如双重循环里所使用的变量)

建议:一次性初始化具名常量;用可执行代码来初始化变量;

建议:打开编译器自动初始化变量;

建议:查看编译器警告;(哪些变量未初始化或未使用)

建议:在为变量赋值前,确保参数合理;

作用域

控制变量的作用域,将避免许多难以察觉的错误,并有助于调试代码;

 建议:使变量引用局部化;缩小攻击窗口(同一变量多个引用点之间的代码);集中引用点可提高程序可读性;

 建议:缩短变量的存活时间(变量存在期间所跨越的语句总数)缩小攻击窗口(同一变量多个引用点之间的代码);集中引用点可提高程序可读性;

 建议:循环里使用的变量尽可能靠近循环语句进行初始化

                      int  a=15;

                      while(a<20){
                       }

 建议:直到变量即将被使用时赋值;

 建议:相关语句放在一起;

 建议:把相关语句组提取成子程序;。建议:尽量使变量局部化;因为扩展变量作用范围更简单,但缩小变量作用范围则很困难;

持续性

绑定时间

把变量和它的值绑定在一起的时间,越晚的绑定时间越有利

                     编码时—使用神秘数值;

     编译时—使用具名常量;

                    加载时—从windows注册表,java属性文件等外部数据源读取数据;

                    对象实例化时—例如创建对象时绑定;

                    即时—每次创建对象时绑定;

数据类型和控制结构之间的关系

序列型数据翻译为程序中的顺序执行,由一些按照特定顺序使用的数据组成

选择型数据翻译为程序中的if和else语句,一组数据在特定时刻有且仅有一项被使用,相应的程序语句必须做出实际的选择;

迭代型数据翻译为程序中的循环 for,while,repeat,是需要反复进行操作的同类型的数据;

为变量指定单一用途

可以使变量的使用变得清晰可见,并且无二义性;

建议:每个变量只用于单一用途;

建议:避免让代码具有隐含含义;(避免混合耦合)

建议:确保使用了所有已声明的变量;