这个专栏会对Java一些常用的神坑做一个总结,希望能够帮助大家更好的填坑。话不多说,进入第一篇的话题:谈谈司空见惯的NPE
目录
1. 什么是NPE(空指针异常)
空指针异常是指程序在运行过程中访问到了内存区域里面的为空的区域,或者是受保护的区域,或者是其他未知的区域,程序在此类区域上执行相关操作时,没有办法执行下去,就报了错误,导致程序中断的一种异常。
2.空指针异常的本质
程序代码块访问到了内存里面为空,或者受保护,或者其他未知区域等等,在此类区域上执行相关操作导致程序出现的错误
需要注意的是:空指针异常是一种运行时异常,跟编译时异常不同,程序只有在运行过程中才有可能出现空指针异常。那么就会跟编译时异常不同,该类异常往往通常难以抓住,也不好处理。因此NPE作为Java开发者的老大难问题,既然没办法事事预料到,那就针对经常出现的场景做个总结,然后针对总结做个更好的处理吧。
3. 空指针异常发生的场景
3.1 基本数据类型
基本数据类型的使用,不太会出现NPE,原因是基本数据类型都是直接在内存层面上操作的,在内存中指定区域,然后初始化默认值,然后就可以直接使用啦。
3.2 包装类型
Java为了使基本数据类型和引用类型之间能够更好的操作,就发明了一个名称叫包装类型。顾名思义,就是把基本数据类型给包装成引用类型,然后就能直接使用该引用类型和其他引用类型更好的操作了。Java针对包装类型和基本数据类型的装箱和拆箱都是自动化的,是一种语法糖,因此我们常常就不会太关注这些细节,因为Java已经帮我们做好了。
但是,我要说的是恰恰就是这种自动拆箱的语法糖,在使用的过程中,就有可能会出现NPE,请看下面例子。
Integer i=100;
int ia=i;
System.out.println(ia);
Integer i=null;
int ia=i;
System.out.println(ia);
需要注意的是,包装类型作为一个引用类型,是可以将null赋值给包装类型的,但是在包装类型自动拆箱还原成基本数据类型时,由于上述i为null,在还原基本数据类型就会报NPE,具体原因我们可以看Integer的代码是怎么写的。
/**
* The value of the {@code Integer}.
*
* @serial
*/
private final int value;
/**
* Constructs a newly allocated {@code Integer} object that
* represents the specified {@code int} value.
*
* @param value the value to be represented by the
* {@code Integer} object.
*/
public Integer(int value) {
this.value = value;
}
/**
* Returns the value of this {@code Integer} as an
* {@code int}.
*/
public int intValue() {
return value;
}
Integer包装类型自动拆箱时会调用上述的intValue方法,而该方法是直接返回了上述value属性的值。但由于在上述的Integer i=null,这段代码中,将其null复制给了i,虽然不会报错误,但是由于i是一个null,那么自然在null就不会存在一个value的属性,更不会存在value对应的属性值。同样的道路,在Long,Short,Byte,Boolean等等包装类型都有类似的方法操作。因此在包装类型自动拆箱的过程中,就需要注意包装类型是否为null,然后好防范NPE。
3.3 数组类型
数组类型也是我们Java开发者经常使用到的类型,但数组类型也有可能出现NPE。请看以下的场景。
int[] array=new int[100];
System.out.println(array);
Object[] objs=new Object[100];
System.out.println(objs);
在上述代码段中,由于第一个是基本数据类型,在创建数组的过程,已经自动初始化好了基本数据类型的初始值。因此不会出现NPE。但是,在下面的Object的数组中,虽然已经new好了一个Object的数组,但是由于Object并没有初始化,因此new完了之后,直接访问是会报NPE的。原因是只new了一个数组列表在内存空间,但并没有将其数组元素填充进去,因为数组元素都是null的。上述结论不仅仅使用于Obejct,也适用于包括像String、其他的自定义对象类型、包装类型等等。
当我们试图去获得数组的元素或者获得数组的大小等等时,若数组本身为null,也会报NPE。请看以下场景
Object[] objs=null;
System.out.println(objs.length);
System.out.println(objs[0]);
以上方法都会报NPE,因为本身数组就是null了,你再在null里面操作,那肯定就会报NPE。
3.4 String类型
String类型也是经常出现NPE的地方,请看以下的场景
String str1=new String("Hello World");
String str2=null;
System.out.println(str1.compareTo(str2));
System.out.println(str2.compareTo(str1));
我们会神奇的发现第一个输出为false,第二个就报NPE了。因为是直接将null和字符串HelloWorld做个比较,因此就报了NPE。值得一提的是,String在字符串操作处理的方法不少,而且都没怎么做NPE处理。因此当我们使用诸如equals、subString等等字符串处理方法时,需要格外注意传入的参数字符串是否为null。需要格外针对这些做个null的检查。
3.5 集合
3.6 引用类型
需要注意的是,我们天天不断的创建新的对象。不断的初始化对象,但缺忽略了一点了。对象实例在使用过程中,也是有可能报NPE的,具体请看以下代码。
import java.util.*;
public class NPE {
public static class NpeTest {
/** 基本数据类型 */
private int a;
private double b;
private float c;
private boolean d;
private byte e;
/** 字符串、数组类型 */
private String str;
private int[] array;
/** 引用类型 */
private Object obj;
/** 集合 */
private List<Integer> list;
private Map<Integer, Integer> map;
private Set<Integer> set;
public NpeTest() {
if (str == null) {
str = new String();
}
if (array == null) {
array = new int[100];
}
if (obj == null) {
obj = new Object();
}
if (list == null) {
list = new ArrayList<>();
}
if (map == null) {
map = new HashMap<>();
}
if (set == null) {
set = new HashSet<>();
}
}
public NpeTest(
int a,
double b,
float c,
boolean d,
byte e,
String str,
int[] array,
Object obj,
List<Integer> list,
Map<Integer, Integer> map,
Set<Integer> set) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
this.e = e;
if (str != null) {
str = new String();
}
if (array != null) {
array = new int[100];
}
if (obj != null) {
obj = new Object();
}
if (list != null) {
list = new ArrayList<>();
}
if (map != null) {
map = new HashMap<>();
}
if (set != null) {
set = new HashSet<>();
}
}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
public double getB() {
return b;
}
public void setB(double b) {
this.b = b;
}
public float getC() {
return c;
}
public void setC(float c) {
this.c = c;
}
public boolean isD() {
return d;
}
public void setD(boolean d) {
this.d = d;
}
public byte getE() {
return e;
}
public void setE(byte e) {
this.e = e;
}
public String getStr() {
return str;
}
public void setStr(String str) {
this.str = str;
}
public int[] getArray() {
return array;
}
public void setArray(int[] array) {
this.array = array;
}
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
public List<Integer> getList() {
return list;
}
public void setList(List<Integer> list) {
this.list = list;
}
public Map<Integer, Integer> getMap() {
return map;
}
public void setMap(Map<Integer, Integer> map) {
this.map = map;
}
public Set<Integer> getSet() {
return set;
}
public void setSet(Set<Integer> set) {
this.set = set;
}
}
public static void main(String[] args) {
NpeTest npeTest = new NpeTest();
System.out.println(list);
}
}
当我们把构造函数中的有关所有引用类型的初始化,也就是上述的引用类型new的那一段代码都注释掉,然后运行程序,我们就会发现,报了NPE。原因是当我们new一个个的对象实例时,并不会对对象实例的各种引用类型做个初始化,赋给引用类型的默认值,因此会是null的,那么当我们访问该类属性或者调用了使用该类属性的方法时就会报NPE。因此,针对上述情况,建议在构造函数中完成引用类型的初始化。
3.7 方法
我们在调用方法的过程中,也有可能出现NPE的情况,请看以下例子。
public String getStr(String str)
{
return null;
}
public static void main(String[] args)
{
System.out.println(getStr().length);
}
在上述代码中,故意返回了一个null,然后在输出中调用该方法,本意是返回该字符串的长度,谁知,却报了NPE。这类情况还是比较多见的,值得我们留意。
4. 空指针的处理
针对上述常见的NPE发生的场景,可以作为一个列表来展示,如何针对不同的场景做更好的处理,请看下列列表。
NPE发生层面 | 发生场景 | 原因 | 处理方法 |
基本数据类型 | 无 | 无 | 无 |
包装类型 | 1. 包装类型自动拆箱转为基本数据类型,然后再进行相关处理操作时 | 拆箱时,都会调用诸如intValue()返回该类的一个为value的属性。当包装类型本身为null时,就会报NPE | 当使用包装类型时,包装类型拆箱转为基本数据类型的情况下,就要特别注意是否包装类型为null。 |
数组类型 | 1. new完对象数组之后,直接对该数组进行操作 2. 数组本身为null,要对该数组进行取值或者取数组长度大小等等的操作 | 1. 针对基本数据类型的数组,在new完之后,内存会开辟一段连续的空间,并且将其元素初始化默认值一个个填充进去。因此可以直接对基本类型的数组进行操作。但是对象数组不同,开辟了空间之后,并没有对该数组元素作为一个初始化填充,因此直接操作是会报NPE的。 2. 针对数组一些取值或者取数组长度等等操作,倘若数组本身为null,也是会报NPE的。 | 1. 在new完对象数组之后,记得初始化对象实例,然后将实例作为元素填充。 2. 对数组进行取值或者获知其长度大小前,进行判空操作。 |
String 类型 | 1. 字符串类型提供的诸如equals、subString、contcat等等方法也有报NPE异常的可能 | 1. 字符串类型针对以上的方法并没有提供强制性的参数检查。因此也会有报NPE的可能性 | 1. 在使用字符串的时候,要格外注意调用其各种方法时,传入的参数是否是null,也需要提前检查。 |
集合类型 | |||
引用类型 | 1. 引用类型本身为null,访问其实例的属性或者调用其实例的方法 2. 引用类型不为null,但访问到了其实例属性为null,或者调用了使用了null的实例属性的方法 | 1. 引用类型为null,在其基础上做访问属性值或者调用方法都是会报NPE的。 2. 引用类型不为null,但是其属性为null,也会报NPE。 | 1. 在使用该对象实例前,对该对象实例进行判空,不为空才能进行后续的访问属性或者调用方法等操作。 2. 对象实例new的初始化操作中,倘若类里面有诸如List、Set、String等等引用类型,别忘了在构造函数里面对这类属性做new的初始化。防止之后的访问或者调用报NPE。 |
方法 | 1. 方法返回null,调用方直接使用被调用方返回的值 2. 方法参数传入null,方法内针对参数进行操作 | ·1. 被调用方返回null值给调用方时,倘若调用方要直接拿返回值进行操作就会报NPE。 2. 方法传入参数没有经过null的检查,之后的参数操作等也报了了NPE。 | 1. 被调用方写方法返回值时,千万,千万,千万,不能返回null值给调用方。除非在文档里面有特殊说明。否则不建议这么做、 2. 调用方在调用方法时,应当做一个参数校验的流程,来防止NPE。 |