通常,null的变量、引用和集合在Java代码中很难处理。它们不仅很难辩别,而且处理起来也很复杂.
事实上,在编译时无法识别处理null的任何错误,并在运行时导致NullPointerException异常.
在本教程中,我们将了解在Java中检查处理null的必要性,以及帮助我们避免在代码中检查处理null的各种替代方法.
根据Javadoc对于NullPointerException的定义,当应用程序在需要对象的地方对象为null时被抛出,例如:
1. 调用null对象的实例方法
2. 访问或修改null对象的字段
3. 当它是一个数组时取null的长度
4. 访问或修改null[]
5. 像抛出Throwable一样抛出null
让我们快速看几个导致这种异常的Java代码示例:
示例1:
public void doSomething() {
String result = doSomethingElse();
if (result.equalsIgnoreCase("Success"))
// 处理逻辑
}
}
private String doSomethingElse() {
return null;
}
这里的result为null,在第3行中抛出NPE异常
示例2:
public void doSomething() {
User user = getUser();
if ("konastin".equalsIgnoreCase(user.getUsername()))
// 处理逻辑
}
}
private User getUser() {
return null;
}
第二行中的我们在获取user对象可能会获取null,第三行中抛出NPE异常
示例3:
public static void main(String[] args) {
findMax(null);
}
private static void findMax(int[] arr) {
int max = arr[0];
//check other elements in loop
}
这个例子第6行中抛出NPE异常
因此,访问null对象的任何字段、方法或索引都会导致NullPointerException,从上面的示例中看到这些.
避免NullPointerException的一种常见方法是检查null:
public void doSomething() {
User user = getUser();
if (user!=null&&"konastin".equalsIgnoreCase(user.getUsername()))
// 处理逻辑
}
}
private User getUser() {
return null;
}
这里第三行进行null检查.
在真是开发中,程序员很难确定哪些对象为null.**一个非常安全的策略是为每个对象检查null.然而,这将导致大量冗余的null检查,并降低代码的可读性.**下面我们将通过Java中的一些替代方法来避免这种冗余.
正如上一节所讨论的,访问null对象的方法或变量会导致NullPointerException。我们还讨论了在访问对象之前对其进行null检查可以消除NullPointerException的可能性
然而,通常有一些api可以处理空值。例如:
public void print(Object param) {
System.out.println("Printing " + param);
}
public Object process() throws Exception {
Object result = doSomething();
if (result == null) {
throw new Exception("Processing fail. Got a null response");
} else {
return result;
}
}
print()方法调用只会打印“null”,但不会抛出异常。类似地,process()在其响应中永远不会返回null。它会抛出异常。
因此,对于访问上述api的客户机代码,不需要进行null检查。
但是,这些api必须在它们的规则中明确表示。api发布此类规则的常见位置是JavaDoc。
然而,这并没有给出API规则的明确指示,因此依赖于客户机代码开发人员来确保其遵从。
Static code analysis 工具可以极大地提高代码质量. 一些这样的工具还允许开发人员维护null规则。FindBugs就是一个例子。
FindBugs通过@Nullable和@NonNull注释帮助管理空规则。我们可以对任何方法、字段、局部变量或参数使用这些注释。这使得带注释的类型是否可以为空对客户机代码显式。我们来看一个例子:
public void accept(@Nonnull Object param) {
System.out.println(param.toString());
}
这里@NonNull清楚地表明,参数不能为空。如果客户机代码调用此方法而没有检查null参数,FindBugs将在编译时生成警告。
开发人员通常依赖IDE编写Java代码。而诸如智能代码完成和有用的警告(比如可能没有分配变量)等特性,肯定在很大程度上有所帮助。
一些IDE还允许开发人员管理API规则,从而消除了对静态代码分析工具的需求。IntelliJ IDEA提供了@NonNull和@Nullable注解。要在IntelliJ中添加对这些注解的支持,我们必须添加以下Maven依赖:
org.jetbrains
annotations
16.0.2
IntelliJ将生成一个警告,如果缺少null检查,就像我们上一个例子中那样。
IntelliJ还为处理复杂的API规则提供了一个规则注解。
到目前为止,我们只讨论了从客户机代码中删除null检查的必要性。但是,这在实际应用中很少适用。
现在,假设我们使用的API不能接受null参数,或者能够返回必须由客户机处理的null响应。这就需要检查参数或null值的响应。
在这里,我们可以使用Java断言代替传统的null检查条件语句:
public void accept(Object param){
assert param != null;
doSomething(param);
}
第2行中,我们检查一个空参数。如果启用断言,这将导致AssertionError。
虽然这是断言非空参数等先决条件的好方法,但是这种方法有两个主要问题:
JVM中通常禁用断言
错误断言会导致无法恢复的uncheck error
因此,不建议新手程序员使用断言检查条件。在接下来的几节中,我们将讨论处理null检查的其他方法。
当我们开发程序时,可以通过提前进行NUll检查,当真实出现null时,这个检查会起作用,抛出NPE异常.
下面我会提供2个版本的代码,第一个goodAccept是较好的风格
public void goodAccept(String one, String two, String three) {
if (one == null || two == null || three == null) {
throw new IllegalArgumentException();
}
process(one);
process(two);
process(three);
}
public void badAccept(String one, String two, String three) {
if (one == null) {
throw new IllegalArgumentException();
} else {
process(one);
}
if (two == null) {
throw new IllegalArgumentException();
} else {
process(two);
}
if (three == null) {
throw new IllegalArgumentException();
} else {
process(three);
}
}
另外,我们还可以使用Guava的先决条件来验证API参数。
由于int基本类型不能接收null,所以我们应该倾向于使用它的包装类Integer
如下思考两个函数:
public static int primitiveSum(int a, int b) {
return a + b;
}
public static Integer wrapperSum(Integer a, Integer b) {
return a + b;
}
我们在调用端调用这两个函数:
int sum = primitiveSum(null, 2);
这将导致编译时错误,因为null不是int的有效值。
当使用包装类的函数(API)时,我们得到一个NullPointerException:
assertThrows(NullPointerException.class, () -> wrapperSum(null, 2));
有时候,我们需要返回一个集合作为方法的返回值。对于这样的方法,我们应该返回一个空集合,而不是null:
public List names() {
if (userExists()) {
return Stream.of(readName()).collect(Collectors.toList());
} else {
return Collections.emptyList();
}
}
因此,我们避免了在调用此方法时的null检查。
Java 7引入的新的Objects API。这个API有几个静态实用程序方法,可以帮我们删除我们代码中大量冗余代码(检查null的代码)。让我们来看一个这样的方法requireNonNull():
public void accept(Object param) {
Objects.requireNonNull(param);
// doSomething()
}
下面测试accept() 方法:
assertThrows(NullPointerException.class, () -> accept(null));
因此,如果null作为参数传递,accept()将抛出NullPointerException。
该类还具有isNull()和nonNull()方法,可以将它们用作判断,来检查对象是否为null。
Java 8在该语言中引入的一个新的可选API。与null相比,Optional为处理NPE提供了更好的规则。让我们看看可选如何消除null检查的需要:
public Optional
private String doSomething(boolean processed) {
if (processed) {
return "passed";
} else {
return null;
}
}
通过返回一个Optional, process()方法向调用者表明,响应可以为空,并且必须在编译时处理。
这显著地消除了客户端代码中任何空检查的需要。空响应可以使用可选API的声明式以不同的方式处理:
assertThrows(Exception.class, () -> process(false).orElseThrow(() -> new Exception()));
此外,它还为API开发人员提供了更好的规则,以便向客户端表明API可以返回空响应。
虽然我们消除了对这个API调用者进行null检查的需要,但是我们使用它来返回一个空响应。为了避免这种情况, Optional 提供了一个ofNullable方法 ,这个方法会返回一个指定的值或者当出现null时返回empty:
public Optional
Lombok是一个很6的lib库,可以减少项目中的代码量。它附带了一组注解,这些注解代替了我们在Java应用程序中经常编写的常见代码部分,例如getter、setter和toString()等。
另外它的其中的一个注解 @NonNull.因此,如果一个项目已经使用Lombok,可以使用@NonNull代替null检查。
在我们继续看一些例子之前,让我们为Lombok添加一个Maven依赖项:
org.projectlombok
lombok
1.18.6
在需要的地方使用@NonNull
public void accept(@NonNull Object param){
System.out.println(param);
}
因此,我们只是注解了需要null检查的对象,Lombok生成编译后的类:
public void accept(@NonNull Object param) {
if (param == null) {
throw new NullPointerException("param");
} else {
System.out.println(param);
}
}
如果param为null,这个方法将抛出NullPointerException。方法必须在其按照@NonNull来显式地实现这一点,客户机代码必须处理异常。
通常,字符串验证除了空值外,还包括对空值的检查。因此,一个通用的验证语句应该是:
public void accept(String param){
if (null != param && !param.isEmpty())
System.out.println(param);
}
如果我们必须处理许多字符串类型,这很快就会变得多余。这就是StringUtils派上用场的地方。在看实际操作之前,让我们为commons-lang3添加一个Maven依赖
org.apache.commons
commons-lang3
3.8.1
现在让我们用StringUtils重构上面的代码:
public void accept(String param) {
if (StringUtils.isNotEmpty(param))
System.out.println(param);
}
因此,我们用静态实用程序方法isNotEmpty()替换了null或empty检查。这个API提供了其他强大的实用程序方法来处理公共字符串函数.
在本文中,我们研究了NullPointerException的各种原因,以及它难以识别的原因。然后,我们学习了在真实开发时,如何处理NPE,以及如何避免null检查,以此优化代码.java新手很值得学习.