简介
任何开发人员在开始他们的Java编程之旅时,都不可避免地会遇到一个错误:NullPointerException。NullPointerException是初级程序员在处理Java中的对象时最常遇到的错误之一。
那么到底什么是NullPointerException?这种错误到底是如何发生的?有什么方法可以避免这些错误?
为了回答这些问题,我们将讨论以下几个概念。
- 原始类型与引用类型
- 空值
- NullPointerException错误
- 防止NullPointerExceptions的策略
原始类型与引用类型
为了正确地讨论NullPointerExceptions,我们必须首先回顾一下原始类型和引用类型之间的区别。
原始数据类型是具有由Java编程语言决定的内置值的数据类型。当被分配到一个原始类型的值时,一个变量会存储实际值本身。例如,语句。
int num = 0;
将整数值 "0 "存储到原始类型变量 "num "中。
原始数据类型通常用小写字母表示,如:。
- int
- 双数
- boolean
- float
- 字节
- 字符
相比之下,引用类型存储的是对象的地址,而不是实际值。当在Java中调用时,这些变量引用了一个对象的内存地址。语句就是一个例子。
String s = "Hello, World!";
在这种情况下,变量s是一个引用类型,指向字符串字面 "Hello, World!"的内存地址。
引用数据类型通常是类,可以是用户定义的,也可以是Java编程语言中内置的。例子包括。
- 字符串
- 阵列列表
- 大整数
- 数组(原始或参考类型的)。
那么这和NullPointerException有什么关系呢?
与原始类型不同,引用类型可以被声明为空。
空值
Java中的空值表示一个不存在的值。
空值可以在赋值语句中被分配给引用类型。
String s = null;
为一个变量赋值为null,本质上是在说:"我做了一个变量,指向什么都没有。"
如前所述,数组是引用类型。因此,指向数组的变量也可以被指定为null。
int[] nums = null;
NullPointerException
在讨论了原始/引用类型和空值的概念后,我们现在终于可以讨论一下NullPointerExceptions了。
NullPointerException发生在使用或访问一个实际为空的引用变量时。
这可能发生在几个方面,包括。
- 调用一个与空对象相关的方法
- 访问或修改空对象的属性
NullPointerException最简单的发生方式之一就是下面这个场景。
例子 1:
public class Main {
public static void main(String[] args) {
// object initialized to "null"
String s = null;
// the line attempts to get the
// first character of the string,
// resulting in a NullPointerException
System.out.println(s.charAt(0));
}
}
如果你要编译并运行上述代码,终端输出会是这样的。
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.charAt(int)" because "<local1>" is null
at Main.main(Main.java:9)
错误告诉我们Main.java:9(代码中的第9行)试图对空对象s调用charAtString方法,因为字符串对象的初始化值为null,这一行将引起NullPointerException。
同样地,如果一个空值被传递到一个假定它是一个对象的函数中,也会产生一个NullPointerException。
例2:
import java.util.ArrayList;
public class Main {
/**
* Puts a number into the list if it
* doesn't already exist.
* @param nums - an ArrayList of numbers
* @param number - some intger to put into the list
*/
private static void putNumber(ArrayList<Integer> nums, int number) {
// checks if number exists in list,
// then puts the number into the
// list
if (!nums.contains(number)) {
nums.add(number);
}
}
public static void main(String[] args) {
// the ArrayList is actually null
ArrayList<Integer> nums = null;
putNumber(nums, 10);
}
}
编译和运行代码会产生以下错误。
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.ArrayList.contains(Object)" because "<parameter1>" is null
at Main.putNumber(Main.java:14)
at Main.main(Main.java:23)
这个错误告诉我们,错误起源于代码的第14行(属于我们定义的函数putNumber())。因为我们传递了一个空对象作为参数,所以该函数在试图对nums调用*add()*时抛出了一个错误。
然而,事情并不总是如此简单,比如这段代码。
例3。
import java.util.Random;
public class Main {
/**
* Checks if the numbers are all odd.
* @param nums - the array holding the numbers.
* @return true if all numbers are odd, false otherwise.
*/
private static boolean isAllOdd(int[] nums) {
// if any of the numbers has a remainder
// of 0 when dividing by 2 (aka it's even),
// then return false (they're not all odd)
for (int i = 0; i < nums.length; i++) {
if (nums[i] % 2 == 0) {
return false;
}
}
// return true (they're all odd numbers)
return true;
}
/**
* Function intended to initialize an array
* of size "size" with random numbers.
* @param nums
* @param size
*/
private static void initializeNums(int[] nums, int size) {
// initialize the nums array, setting it from
// null to an actual object
nums = new int[size];
// initialize random object to generate random
// integers
Random random = new Random();
// generate numbers until the nums array
// is filled with integers
for (int i = 0; i < nums.length; i++) {
nums[i] = random.nextInt();
}
}
public static void main(String[] args) {
// initialized to null
int[] numbers = null;
// initialized to some array of integers
initializeNums(numbers, 10);
// it should print either "true"
// or "false", right???
// right????????
System.out.println(isAllOdd(numbers));
}
}
乍一看,这段代码似乎完全没有问题。我们似乎已经通过将numbers数组初始化为一个实际的10个数字的数组来解决null的问题。因此,在编译和运行这段代码后,终端应该能够打印出 "真 "或 "假"。
但它没有。
Exception in thread "main" java.lang.NullPointerException: Cannot read the array length because "<parameter1>" is null
at Main.isAllOdd(Main.java:13)
at Main.main(Main.java:55)
我们并不清楚为什么这个例子会输出一个NullPointerException。毕竟,这段代码清楚地将参考数字传递给了*initializeNums()*方法。
这是因为Java是按值传递,而不是按引用传递。
这意味着什么呢?
它意味着,对于一个给定的函数(例如,我们的initializeNums()函数),Java实际上为函数参数分配了新的内存(它为nums和size创建了空间)。
这对size参数来说很好,因为我们只关心实际值。然而,nums参数是被初始化的,而不是原来的numbers数组。因此,由于numbers仍然是空的,在numbers上调用isAllOdd()会导致一个NullPointerException。

例四
这段代码是怎么回事?
public class Main {
public static void main(String[] args) {
// initializes an array of empty strings, right?
String[] array = new String[10];
// an entry in the array should be an empty string,
// right?
System.out.println(array[0].equals(""));
}
}
乍看之下,这段代码似乎没有问题。因为数组被初始化为一个大小为10的字符串数组,每个条目都应该有一个空字符串。因此,将数组中的第一个条目与空字符串相比较,应该打印出 "true",对吗?
不是的。
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "<local1>[0]" is null
at Main.main(Main.java:8)
数组中的每个条目都是一个引用类型。原始类型数组中的条目被自动初始化为某个值(由于原始数据类型是预先定义的);然而,引用类型数组中的条目只是被赋予空值。因此,一个初始化的引用类型数组实际上将包含所有空值的条目。
处理NullPointerExceptions的策略
说到这里,程序员有什么方法可以捕捉到不应该出现的空值呢?
首先,我们可以对任何使用引用类型的函数进行检查。
以例2为例。我们可以在*putNumber()*函数前添加if语句,检查是否有任何引用类型的参数为空。
利用空值检查
import java.util.ArrayList;
public class Main {
/**
* Puts a number into the list if it
* doesn't already exist.
* @param nums - an ArrayList of numbers
* @param number - some intger to put into the list
*/
private static void putNumber(ArrayList<Integer> nums, int number) {
// checks if the list is null. If yes,
// then return without doing anything
// (or handle it some other way)
if (nums == null) {
return;
}
// checks if number exists in list,
// then puts the number into the
// list
if (!nums.contains(number)) {
nums.add(number);
}
}
public static void main(String[] args) {
// the ArrayList is actually null
ArrayList<Integer> nums = null;
putNumber(nums, 10);
}
}
如果我们运行这段代码,NullPointerException就会被避免,或者程序员可能会选择在传递空对象时做其他事情(即他们可能会抛出某种异常)。
另一种避免NullPointerExceptions的方法是利用Object.requireNonNull()函数。这要求传递到方法中的任何对象必须不是空的;否则,该方法将抛出一个NullPointerException。
Objects.requireNonNull()
import java.util.ArrayList;
import java.util.Objects;
public class Main {
/**
* Puts a number into the list if it
* doesn't already exist.
* @param nums - an ArrayList of numbers
* @param number - some intger to put into the list
*/
private static void putNumber(ArrayList<Integer> nums, int number) {
// checks if the list is null. If yes,
// then the method will throw a NullPointerException.
Objects.requireNonNull(nums);
// checks if number exists in list,
// then puts the number into the
// list
if (!nums.contains(number)) {
nums.add(number);
}
}
public static void main(String[] args) {
// the ArrayList is actually null
ArrayList<Integer> nums = null;
putNumber(nums, 10);
}
}
当空值列表被传递到Objects.requireNonNull()中时(并且抛出了一个异常),程序员可以精确地指出异常发生的位置,并找出空值的来源。诸如此类的空值检查对于那些还没有遇到像例3中那样的微妙bug的Java新手来说尤其宝贵。
程序员也可以在编码时采取预防措施,确保引用变量被正确初始化。以例3为例。既然我们现在知道这段代码不会工作,那么我们能做什么呢?
一个解决方案是这样的:我们可以返回初始化的数组,并将原数组设置为上述返回值,而不是在函数内部初始化数组,就像这样。
import java.util.Random;
public class Main {
/**
* Checks if the numbers are all odd.
* @param nums - the array holding the numbers.
* @return true if all numbers are odd, false otherwise.
*/
private static boolean isAllOdd(int[] nums) {
// if any of the numbers has a remainder
// of 0 when dividing by 2 (aka it's even),
// then return false (they're not all odd)
for (int i = 0; i < nums.length; i++) {
if (nums[i] % 2 == 0) {
return false;
}
}
// return true (they're all odd numbers)
return true;
}
/**
* Function intended to initialize an array
* of size "size" with random numbers.
* @param nums
* @param size
* @return int[] of initialized numbers.
*/
private static int[] initializeNums(int size) {
// initialize the nums array, setting it from
// null to an actual object
int[] nums = new int[size];
// initialize random object to generate random
// integers
Random random = new Random();
// generate numbers until the nums array
// is filled with integers
for (int i = 0; i < nums.length; i++) {
nums[i] = random.nextInt();
}
return nums;
}
public static void main(String[] args) {
// initialized to null
int[] numbers = null;
// the original array is set equal to
// the initialized array
numbers = initializeNums(10);
// now it should print "true" or
// "false"
System.out.println(isAllOdd(numbers));
}
}
这又会产生 "真 "或 "假",取决于你的程序随机选择什么数字。不管怎么说,重要的是成功避免了NullPointerException。
此外,除了数组本身之外,引用型数组中的每个条目都应该被初始化。以重新设计的例4为例。
public class Main {
public static void main(String[] args) {
// initializes a String array of size 10
String[] array = new String[10];
// initializes EACH entry of the String array
for (int i = 0; i < array.length; i++) {
array[i] = "";
}
// now, the console should print "true"
System.out.println(array[0].equals(""));
}
}
初始化String数组中的每个条目可以确保这些条目不为空。反过来,也就避免了NullPointerException的发生。
注意:现在的开发环境,如Visual Studio Code和IntelliJ,都有可以检测可能的NullPointerExceptions的功能。通常,某个变量或某行代码会被高亮显示,并发出警告,大意是:"这可能导致NullPointerException"。
结论
那么我们今天学到了什么?
- 原始类型存储实际值,而引用类型存储对象的地址。
- 引用类型包括内置类(String、ArrayList等)、UESR定义的类,以及原始或引用值的数组等项目。
- 空值是指没有分配给一个引用变量的值。
- 原始类型不能有空值;相反,引用类型可以有空值。
- 当使用或访问一个实际上已被赋值为空的引用变量时,就会发生NullPointerException。
- 程序员可以使用各种策略来处理NullPointerExceptions,如null检查、Object.requireNonNull(),以及其他有意识的编程实践。
回顾一下
问题1
以下哪个不是引用类型的例子?
字符串
双重
原始类型的数组
阵列列表
Java中的*double*类型是一种原始类型。字符串、数组和ArrayLists是引用类型。
问题2
真的还是假的?原始类型可以有空值
真
错
只有引用类型可以被初始化为null。
通过OpenGenus的这篇文章,你一定对Java中的NullPointerException有了完整的了解。