Java中的NullPointerException

786 阅读9分钟

简介

任何开发人员在开始他们的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!"的内存地址。

Blank-diagram--1-

引用数据类型通常是类,可以是用户定义的,也可以是Java编程语言中内置的。例子包括。

  • 字符串
  • 阵列列表
  • 大整数
  • 数组(原始或参考类型的)。

那么这和NullPointerException有什么关系呢?

与原始类型不同,引用类型可以被声明为空

空值

Java中的空值表示一个不存在的值

空值可以在赋值语句中被分配给引用类型。

String s = null;

Blank-diagram--2-

为一个变量赋值为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。

Blank-diagram--3-

同样地,如果一个空值被传递到一个假定它是一个对象的函数中,也会产生一个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实际上为函数参数分配了新的内存(它为numssize创建了空间)。

这对size参数来说很好,因为我们只关心实际值。然而,nums参数是被初始化的,而不是原来的numbers数组。因此,由于numbers仍然是空的,在numbers上调用isAllOdd()会导致一个NullPointerException

Blank-diagram--7-

例四

这段代码是怎么回事?

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就会被避免,或者程序员可能会选择在传递空对象时做其他事情(即他们可能会抛出某种异常)。

Blank-diagram--6-

另一种避免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有了完整的了解。